import { CommonRange, isIntersectRanges } from "@tingwu/common";
import { Model } from "../../model";
import { PID, Sentence, SID, UID, WID, Word } from "../../persist";
import { Point, Range as SelectionRange } from "../../editor";

export class Book {
  model: Model;

  cachedWords: Record<
    PID,
    {
      version: number | undefined;
      words: Word[];
    }
  > = {};

  constructor(model: Model) {
    this.model = model;
  }

  getPids() {
    return this.model.book.transcription.pids;
  }

  // There is no cached support.
  getParagraphs() {
    return this.getPids().map(pid => this.getParagraph(pid));
  }

  getParagraph(pid: PID) {
    return this.model.book.transcription.paragraphs[pid];
  }

  getParagraphByWid(wid: WID) {
    const paragraphs = this.getParagraphs();
    const { length } = paragraphs;

    for (let i = 0; i < length; i++) {
      const paragraph = paragraphs[i];
      if (paragraph.wids.indexOf(wid) > -1) {
        return paragraph;
      }
    }
    return undefined;
  }

  getParagraphBySid(sid: SID) {
    const paragraphs = this.getParagraphs();
    const { length } = paragraphs;

    for (let i = 0; i < length; i++) {
      const paragraph = paragraphs[i];
      const words = this.getWords(paragraph.pid);
      const wordLength = words.length;

      for (let j = 0; j < wordLength; j++) {
        const word = words[j];
        if (word.sentenceId === sid) {
          return paragraph;
        }
      }
    }
    return undefined;
  }

  private getCurrentWords(pid: PID) {
    const { paragraphs, words: allWords } = this.model.book.transcription;
    if (!paragraphs[pid]) {
      return [];
    }
    const words: Word[] = [];
    paragraphs[pid].wids.forEach(wid => {
      const word = allWords[wid];
      if (word) {
        words.push(word);
      }
    });

    return words;
  }

  getWords(pid: PID) {
    const cache = this.cachedWords[pid];
    const version = this.model.book.paragraphVersions[pid];
    if (!cache || cache.version !== version) {
      const words = this.getCurrentWords(pid);
      if (words.length === 0) {
        // `modelValue` was not ready, dont goto cache.
        return [];
      }
      this.cachedWords[pid] = {
        words,
        version,
      };
    }
    return this.cachedWords[pid].words;
  }

  getWordsByWids(wids: WID[]) {
    const words: Word[] = [];
    wids.forEach(wid => {
      const word = this.model.book.transcription.words[wid];
      if (word) {
        words.push(word);
      }
    });
    return words;
  }

  getWord(wid: WID) {
    return this.model.book.transcription.words[wid];
  }

  getTextContent(pid: PID) {
    return this.getWords(pid)
      .map(w => w.text)
      .join("");
  }

  getTextContentWithoutNfixing(pid: PID) {
    return this.getWords(pid)
      .filter(w => !w.nFix)
      .map(w => w.text)
      .join("");
  }

  getWordByOffset(pid: PID, offset: number) {
    const words = this.getWords(pid);
    let foundWord: Word | undefined;
    let pos = 0;
    words.some(word => {
      pos += word.text.length;
      if (pos > offset) {
        foundWord = word;
        return true;
      }
      return false;
    });
    return foundWord;
  }

  getAllTextContent() {
    return this.getPids().map(pid => this.getTextContent(pid));
  }

  getSentencesBySids(sids: SID[]) {
    const pids = this.getPids();
    const sentences: Record<SID, Sentence> = {};
    pids.forEach(pid => {
      const words = this.getWords(pid);
      words.forEach(word => {
        if (sids.indexOf(word.sentenceId) > -1) {
          if (!sentences[word.sentenceId]) {
            sentences[word.sentenceId] = {
              sid: word.sentenceId,
              wids: [word.wid],
            };
          } else {
            sentences[word.sentenceId].wids.push(word.wid);
          }
        }
      });
    });
    return sentences;
  }

  getFirstWordBySid(sid: SID) {
    const sentenceMap = this.getSentencesBySids([sid]);
    const sentence = sentenceMap[sid];
    if (!sentence) {
      return undefined;
    }
    const { wids } = sentence;
    return this.getWord(wids[0]);
  }
  getParagraphAgendaByStartTimestamp(time: number) {
    const paragraphs = this.getParagraphs();
    return paragraphs.filter(paragraph => {
      const words = this.getWords(paragraph.pid);
      if (words.length === 0) {
        return false;
      }

      const { beginTime } = words[0];
      const { endTime } = words[words.length - 1];
      if (time >= beginTime && time <= endTime) {
        return true;
      }
      return false;
    });
  }

  getParagraphByTimestampRange(range: CommonRange) {
    const paragraphs = this.getParagraphs();
    return paragraphs.filter(paragraph => {
      const words = this.getWords(paragraph.pid);
      if (words.length === 0) {
        return false;
      }

      const { beginTime } = words[0];
      const { endTime } = words[words.length - 1];

      const paragraphRange: CommonRange = {
        start: beginTime,
        end: endTime,
      };
      return isIntersectRanges(range, paragraphRange);
    });
  }

  isAppearedWid(wid: WID) {
    return this.model.book.isAppearedWid(wid);
  }

  isUserChangedSentence(sid: SID) {
    return this.model.book.isUserChangedSentence(sid);
  }

  getLatestSid() {
    const paragraphs = this.getParagraphs();

    for (let i = paragraphs.length - 1; i >= 0; i--) {
      const paragraph = paragraphs[i];
      if (paragraph.wids.length > 0) {
        const word = this.getWord(paragraph.wids[paragraph.wids.length - 1]);
        if (word) {
          return word.sentenceId;
        }
      }
    }
    return undefined;
  }

  getAllUidsArrayAndMap() {
    const paragraphs = this.getParagraphs();
    const uids: UID[] = [];
    const uidsMap: Record<UID, boolean> = {};
    paragraphs.forEach(paragraph => {
      const { uid } = paragraph;
      if (uid === undefined) return;
      if (uidsMap[uid] == null) {
        uidsMap[uid] = true;
        uids.push(uid);
      }
    });
    return { uids, uidsMap };
  }

  getAllUids() {
    return this.getAllUidsArrayAndMap().uids;
  }

  getAllUidsMap() {
    return this.getAllUidsArrayAndMap().uidsMap;
  }

  getPidByWid(wid: WID): PID | undefined {
    const pids = this.getPids();
    const found = pids.find(pid => {
      const paragraph = this.getParagraph(pid);
      return paragraph && paragraph.wids.includes(wid);
    });
    return found;
  }

  getWordRangeByOffset(pid: PID, offset: number) {
    const words = this.getWords(pid);

    let pos = 0;
    let foundRange: SelectionRange | null = null;
    words.some(word => {
      const nextPos = pos + word.text.length;
      if (offset >= pos && offset <= nextPos) {
        foundRange = SelectionRange.create({
          start: Point.create(pid, pos),
          end: Point.create(pid, nextPos),
        });
        return true;
      }

      pos = nextPos;
      return false;
    });
    return foundRange;
  }
}
