import { createEbmlTag } from 'src/factory';
import { decodeEbmlTagHeader } from '../decode-utils';
import { EbmlElementType, EbmlTagPosition, isEbmlMasterTagId } from './enums';
import type { EbmlMasterTagIdType } from './enums';
import {
  type CreateEbmlTagOptions,
  type DecodeContentOptions,
  EbmlTagTrait,
} from './tag-trait';

export interface CreateEbmlMasterTagOptions
  extends Omit<CreateEbmlTagOptions, 'position' | 'type' | 'id'> {
  id: EbmlMasterTagIdType;
}

export class EbmlMasterTag extends EbmlTagTrait {
  private _children: EbmlTagTrait[] = [];

  constructor(options: CreateEbmlMasterTagOptions) {
    super({
      ...options,
      id: options.id,
      type: EbmlElementType.Master,
    });
  }

  override get byteLengthQueuingSize(): number {
    if (this.position === EbmlTagPosition.Start) {
      return this.headerLength;
    }
    return 0;
  }

  get children(): EbmlTagTrait[] {
    return this._children;
  }

  set children(value: EbmlTagTrait[]) {
    this._children = value;
  }

  *encodeContent(): Generator<Uint8Array, void, unknown> {
    for (const child of this.children) {
      yield* child.encode();
    }
  }

  async *decodeContentImpl(options: DecodeContentOptions) {
    const controller = options.dataViewController;
    const collectChild = options.collectChild;
    while (true) {
      const offset = controller.getOffset();

      if (offset >= this.endOffset) {
        break;
      }

      const peeked = await controller.peek(offset);

      if (!peeked) {
        break;
      }

      const vints = await decodeEbmlTagHeader(controller);

      const { tagId, tagVint, sizeVint } = vints;

      const headerLength = tagVint.length + sizeVint.length;
      const contentLength = sizeVint.value;

      const isMaster = isEbmlMasterTagId(tagId);

      if (isMaster) {
        const tag: EbmlTagTrait = createEbmlTag(tagId, {
          headerLength,
          contentLength,
          startOffset: offset,
          position: EbmlTagPosition.Start,
          parent: this,
        });
        yield tag;
      }

      await controller.seek(offset + headerLength);

      const tag: EbmlTagTrait = createEbmlTag(tagId, {
        headerLength,
        contentLength,
        startOffset: offset,
        position: isMaster ? EbmlTagPosition.End : EbmlTagPosition.Content,
        parent: this,
      });

      for await (const item of tag.decodeContent(options)) {
        yield item;
      }

      tag.endOffset = controller.getOffset();

      let shouldCollectChild: boolean;
      if (typeof collectChild === 'function') {
        shouldCollectChild = !!collectChild(tag, this);
      } else {
        shouldCollectChild = !!collectChild;
      }

      if (shouldCollectChild) {
        this._children.push(tag);
      }

      yield tag;
    }
  }
}
