/**
 * Copyright(c) Live2D Inc. All rights reserved.
 *
 * Use of this source code is governed by the Live2D Open Software license
 * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html.
 */

import { ICubismModelSetting } from './icubismmodelsetting';
import { CubismIdHandle } from './id/cubismid';
import { CubismFramework } from './live2dcubismframework';
import { CubismJson, Value } from './utils/cubismjson';

export enum FrequestNode {
  FrequestNode_Groups, // getRoot().getValueByString(Groups)
  FrequestNode_Moc, // getRoot().getValueByString(FileReferences).getValueByString(Moc)
  FrequestNode_Motions, // getRoot().getValueByString(FileReferences).getValueByString(Motions)
  FrequestNode_Expressions, // getRoot().getValueByString(FileReferences).getValueByString(Expressions)
  FrequestNode_Textures, // getRoot().getValueByString(FileReferences).getValueByString(Textures)
  FrequestNode_Physics, // getRoot().getValueByString(FileReferences).getValueByString(Physics)
  FrequestNode_Pose, // getRoot().getValueByString(FileReferences).getValueByString(Pose)
  FrequestNode_HitAreas // getRoot().getValueByString(HitAreas)
}

/**
 * Model3Jsonパーサー
 *
 * model3.jsonファイルをパースして値を取得する
 */
export class CubismModelSettingJson extends ICubismModelSetting {
  /**
   * 引数付きコンストラクタ
   *
   * @param buffer    Model3Jsonをバイト配列として読み込んだデータバッファ
   * @param size      Model3Jsonのデータサイズ
   */
  public constructor(buffer: ArrayBuffer, size: number) {
    super();
    this._json = CubismJson.create(buffer, size);

    if (this.getJson()) {
      this._jsonValue = [
        // 順番はenum FrequestNodeと一致させる
        this.getJson().getRoot().getValueByString(this.groups),
        this.getJson()
          .getRoot()
          .getValueByString(this.fileReferences)
          .getValueByString(this.moc),
        this.getJson()
          .getRoot()
          .getValueByString(this.fileReferences)
          .getValueByString(this.motions),
        this.getJson()
          .getRoot()
          .getValueByString(this.fileReferences)
          .getValueByString(this.expressions),
        this.getJson()
          .getRoot()
          .getValueByString(this.fileReferences)
          .getValueByString(this.textures),
        this.getJson()
          .getRoot()
          .getValueByString(this.fileReferences)
          .getValueByString(this.physics),
        this.getJson()
          .getRoot()
          .getValueByString(this.fileReferences)
          .getValueByString(this.pose),
        this.getJson().getRoot().getValueByString(this.hitAreas)
      ];
    }
  }

  /**
   * デストラクタ相当の処理
   */
  public release(): void {
    CubismJson.delete(this._json);

    this._jsonValue = null;
  }

  /**
   * CubismJsonオブジェクトを取得する
   *
   * @return CubismJson
   */
  public getJson(): CubismJson {
    return this._json;
  }

  /**
   * Mocファイルの名前を取得する
   * @return Mocファイルの名前
   */
  public getModelFileName(): string {
    if (!this.isExistModelFile()) {
      return '';
    }
    return this._jsonValue[FrequestNode.FrequestNode_Moc].getRawString();
  }

  /**
   * モデルが使用するテクスチャの数を取得する
   * テクスチャの数
   */
  public getTextureCount(): number {
    if (!this.isExistTextureFiles()) {
      return 0;
    }

    return this._jsonValue[FrequestNode.FrequestNode_Textures].getSize();
  }

  /**
   * テクスチャが配置されたディレクトリの名前を取得する
   * @return テクスチャが配置されたディレクトリの名前
   */
  public getTextureDirectory(): string {
    const texturePath = this._jsonValue[FrequestNode.FrequestNode_Textures]
      .getValueByIndex(0)
      .getRawString();

    const pathArray = texturePath.split('/');
    // 最後の要素はテクスチャ名なので不要
    const arrayLength = pathArray.length - 1;
    let textureDirectoryStr = '';

    // 分割したパスを結合
    for (let i = 0; i < arrayLength; i++) {
      textureDirectoryStr += pathArray[i];
      if (i < arrayLength - 1) {
        textureDirectoryStr += '/';
      }
    }

    return textureDirectoryStr;
  }

  /**
   * モデルが使用するテクスチャの名前を取得する
   * @param index 配列のインデックス値
   * @return テクスチャの名前
   */
  public getTextureFileName(index: number): string {
    return this._jsonValue[FrequestNode.FrequestNode_Textures]
      .getValueByIndex(index)
      .getRawString();
  }

  /**
   * モデルに設定された当たり判定の数を取得する
   * @return モデルに設定された当たり判定の数
   */
  public getHitAreasCount(): number {
    if (!this.isExistHitAreas()) {
      return 0;
    }

    return this._jsonValue[FrequestNode.FrequestNode_HitAreas].getSize();
  }

  /**
   * 当たり判定に設定されたIDを取得する
   *
   * @param index 配列のindex
   * @return 当たり判定に設定されたID
   */
  public getHitAreaId(index: number): CubismIdHandle {
    return CubismFramework.getIdManager().getId(
      this._jsonValue[FrequestNode.FrequestNode_HitAreas]
        .getValueByIndex(index)
        .getValueByString(this.id)
        .getRawString()
    );
  }

  /**
   * 当たり判定に設定された名前を取得する
   * @param index 配列のインデックス値
   * @return 当たり判定に設定された名前
   */
  public getHitAreaName(index: number): string {
    return this._jsonValue[FrequestNode.FrequestNode_HitAreas]
      .getValueByIndex(index)
      .getValueByString(this.name)
      .getRawString();
  }

  /**
   * 物理演算設定ファイルの名前を取得する
   * @return 物理演算設定ファイルの名前
   */
  public getPhysicsFileName(): string {
    if (!this.isExistPhysicsFile()) {
      return '';
    }

    return this._jsonValue[FrequestNode.FrequestNode_Physics].getRawString();
  }

  /**
   * パーツ切り替え設定ファイルの名前を取得する
   * @return パーツ切り替え設定ファイルの名前
   */
  public getPoseFileName(): string {
    if (!this.isExistPoseFile()) {
      return '';
    }

    return this._jsonValue[FrequestNode.FrequestNode_Pose].getRawString();
  }

  /**
   * 表情設定ファイルの数を取得する
   * @return 表情設定ファイルの数
   */
  public getExpressionCount(): number {
    if (!this.isExistExpressionFile()) {
      return 0;
    }

    return this._jsonValue[FrequestNode.FrequestNode_Expressions].getSize();
  }

  /**
   * 表情設定ファイルを識別する名前（別名）を取得する
   * @param index 配列のインデックス値
   * @return 表情の名前
   */
  public getExpressionName(index: number): string {
    return this._jsonValue[FrequestNode.FrequestNode_Expressions]
      .getValueByIndex(index)
      .getValueByString(this.name)
      .getRawString();
  }

  /**
   * 表情設定ファイルの名前を取得する
   * @param index 配列のインデックス値
   * @return 表情設定ファイルの名前
   */
  public getExpressionFileName(index: number): string {
    return this._jsonValue[FrequestNode.FrequestNode_Expressions]
      .getValueByIndex(index)
      .getValueByString(this.filePath)
      .getRawString();
  }

  /**
   * モーショングループの数を取得する
   * @return モーショングループの数
   */
  public getMotionGroupCount(): number {
    if (!this.isExistMotionGroups()) {
      return 0;
    }

    return this._jsonValue[FrequestNode.FrequestNode_Motions].getKeys().length;
  }

  /**
   * モーショングループの名前を取得する
   * @param index 配列のインデックス値
   * @return モーショングループの名前
   */
  public getMotionGroupName(index: number): string {
    if (!this.isExistMotionGroups()) {
      return null;
    }

    return this._jsonValue[FrequestNode.FrequestNode_Motions].getKeys()[index];
  }

  /**
   * モーショングループに含まれるモーションの数を取得する
   * @param groupName モーショングループの名前
   * @return モーショングループの数
   */
  public getMotionCount(groupName: string): number {
    if (!this.isExistMotionGroupName(groupName)) {
      return 0;
    }

    return this._jsonValue[FrequestNode.FrequestNode_Motions]
      .getValueByString(groupName)
      .getSize();
  }

  /**
   * グループ名とインデックス値からモーションファイル名を取得する
   * @param groupName モーショングループの名前
   * @param index     配列のインデックス値
   * @return モーションファイルの名前
   */
  public getMotionFileName(groupName: string, index: number): string {
    if (!this.isExistMotionGroupName(groupName)) {
      return '';
    }

    return this._jsonValue[FrequestNode.FrequestNode_Motions]
      .getValueByString(groupName)
      .getValueByIndex(index)
      .getValueByString(this.filePath)
      .getRawString();
  }

  /**
   * モーションに対応するサウンドファイルの名前を取得する
   * @param groupName モーショングループの名前
   * @param index 配列のインデックス値
   * @return サウンドファイルの名前
   */
  public getMotionSoundFileName(groupName: string, index: number): string {
    if (!this.isExistMotionSoundFile(groupName, index)) {
      return '';
    }

    return this._jsonValue[FrequestNode.FrequestNode_Motions]
      .getValueByString(groupName)
      .getValueByIndex(index)
      .getValueByString(this.soundPath)
      .getRawString();
  }

  /**
   * モーション開始時のフェードイン処理時間を取得する
   * @param groupName モーショングループの名前
   * @param index 配列のインデックス値
   * @return フェードイン処理時間[秒]
   */
  public getMotionFadeInTimeValue(groupName: string, index: number): number {
    if (!this.isExistMotionFadeIn(groupName, index)) {
      return -1.0;
    }

    return this._jsonValue[FrequestNode.FrequestNode_Motions]
      .getValueByString(groupName)
      .getValueByIndex(index)
      .getValueByString(this.fadeInTime)
      .toFloat();
  }

  /**
   * モーション終了時のフェードアウト処理時間を取得する
   * @param groupName モーショングループの名前
   * @param index 配列のインデックス値
   * @return フェードアウト処理時間[秒]
   */
  public getMotionFadeOutTimeValue(groupName: string, index: number): number {
    if (!this.isExistMotionFadeOut(groupName, index)) {
      return -1.0;
    }

    return this._jsonValue[FrequestNode.FrequestNode_Motions]
      .getValueByString(groupName)
      .getValueByIndex(index)
      .getValueByString(this.fadeOutTime)
      .toFloat();
  }

  /**
   * ユーザーデータのファイル名を取得する
   * @return ユーザーデータのファイル名
   */
  public getUserDataFile(): string {
    if (!this.isExistUserDataFile()) {
      return '';
    }

    return this.getJson()
      .getRoot()
      .getValueByString(this.fileReferences)
      .getValueByString(this.userData)
      .getRawString();
  }

  /**
   * レイアウト情報を取得する
   * @param outLayoutMap Mapクラスのインスタンス
   * @return true レイアウト情報が存在する
   * @return false レイアウト情報が存在しない
   */
  public getLayoutMap(outLayoutMap: Map<string, number>): boolean {
    // 存在しない要素にアクセスするとエラーになるためValueがnullの場合はnullを代入する
    const map: Map<string, Value> = this.getJson()
      .getRoot()
      .getValueByString(this.layout)
      .getMap();

    if (map == null) {
      return false;
    }

    let ret = false;

    for (const element of map) {
      outLayoutMap.set(element[0], element[1].toFloat());
      ret = true;
    }

    return ret;
  }

  /**
   * 目パチに関連付けられたパラメータの数を取得する
   * @return 目パチに関連付けられたパラメータの数
   */
  public getEyeBlinkParameterCount(): number {
    if (!this.isExistEyeBlinkParameters()) {
      return 0;
    }

    let num = 0;
    for (
      let i = 0;
      i < this._jsonValue[FrequestNode.FrequestNode_Groups].getSize();
      i++
    ) {
      const refI: Value =
        this._jsonValue[FrequestNode.FrequestNode_Groups].getValueByIndex(i);
      if (refI.isNull() || refI.isError()) {
        continue;
      }

      if (refI.getValueByString(this.name).getRawString() == this.eyeBlink) {
        num = refI.getValueByString(this.ids).getVector().length;
        break;
      }
    }

    return num;
  }

  /**
   * 目パチに関連付けられたパラメータのIDを取得する
   * @param index 配列のインデックス値
   * @return パラメータID
   */
  public getEyeBlinkParameterId(index: number): CubismIdHandle {
    if (!this.isExistEyeBlinkParameters()) {
      return null;
    }

    for (
      let i = 0;
      i < this._jsonValue[FrequestNode.FrequestNode_Groups].getSize();
      i++
    ) {
      const refI: Value =
        this._jsonValue[FrequestNode.FrequestNode_Groups].getValueByIndex(i);
      if (refI.isNull() || refI.isError()) {
        continue;
      }

      if (refI.getValueByString(this.name).getRawString() == this.eyeBlink) {
        return CubismFramework.getIdManager().getId(
          refI.getValueByString(this.ids).getValueByIndex(index).getRawString()
        );
      }
    }
    return null;
  }

  /**
   * リップシンクに関連付けられたパラメータの数を取得する
   * @return リップシンクに関連付けられたパラメータの数
   */
  public getLipSyncParameterCount(): number {
    if (!this.isExistLipSyncParameters()) {
      return 0;
    }

    let num = 0;
    for (
      let i = 0;
      i < this._jsonValue[FrequestNode.FrequestNode_Groups].getSize();
      i++
    ) {
      const refI: Value =
        this._jsonValue[FrequestNode.FrequestNode_Groups].getValueByIndex(i);
      if (refI.isNull() || refI.isError()) {
        continue;
      }

      if (refI.getValueByString(this.name).getRawString() == this.lipSync) {
        num = refI.getValueByString(this.ids).getVector().length;
        break;
      }
    }

    return num;
  }

  /**
   * リップシンクに関連付けられたパラメータの数を取得する
   * @param index 配列のインデックス値
   * @return パラメータID
   */
  public getLipSyncParameterId(index: number): CubismIdHandle {
    if (!this.isExistLipSyncParameters()) {
      return null;
    }

    for (
      let i = 0;
      i < this._jsonValue[FrequestNode.FrequestNode_Groups].getSize();
      i++
    ) {
      const refI: Value =
        this._jsonValue[FrequestNode.FrequestNode_Groups].getValueByIndex(i);
      if (refI.isNull() || refI.isError()) {
        continue;
      }

      if (refI.getValueByString(this.name).getRawString() == this.lipSync) {
        return CubismFramework.getIdManager().getId(
          refI.getValueByString(this.ids).getValueByIndex(index).getRawString()
        );
      }
    }
    return null;
  }

  /**
   * モデルファイルのキーが存在するかどうかを確認する
   * @return true キーが存在する
   * @return false キーが存在しない
   */
  protected isExistModelFile(): boolean {
    const node: Value = this._jsonValue[FrequestNode.FrequestNode_Moc];
    return !node.isNull() && !node.isError();
  }

  /**
   * テクスチャファイルのキーが存在するかどうかを確認する
   * @return true キーが存在する
   * @return false キーが存在しない
   */
  protected isExistTextureFiles(): boolean {
    const node: Value = this._jsonValue[FrequestNode.FrequestNode_Textures];
    return !node.isNull() && !node.isError();
  }

  /**
   * 当たり判定のキーが存在するかどうかを確認する
   * @return true キーが存在する
   * @return false キーが存在しない
   */
  protected isExistHitAreas(): boolean {
    const node: Value = this._jsonValue[FrequestNode.FrequestNode_HitAreas];
    return !node.isNull() && !node.isError();
  }

  /**
   * 物理演算ファイルのキーが存在するかどうかを確認する
   * @return true キーが存在する
   * @return false キーが存在しない
   */
  protected isExistPhysicsFile(): boolean {
    const node: Value = this._jsonValue[FrequestNode.FrequestNode_Physics];
    return !node.isNull() && !node.isError();
  }

  /**
   * ポーズ設定ファイルのキーが存在するかどうかを確認する
   * @return true キーが存在する
   * @return false キーが存在しない
   */
  protected isExistPoseFile(): boolean {
    const node: Value = this._jsonValue[FrequestNode.FrequestNode_Pose];
    return !node.isNull() && !node.isError();
  }

  /**
   * 表情設定ファイルのキーが存在するかどうかを確認する
   * @return true キーが存在する
   * @return false キーが存在しない
   */
  protected isExistExpressionFile(): boolean {
    const node: Value = this._jsonValue[FrequestNode.FrequestNode_Expressions];
    return !node.isNull() && !node.isError();
  }

  /**
   * モーショングループのキーが存在するかどうかを確認する
   * @return true キーが存在する
   * @return false キーが存在しない
   */
  protected isExistMotionGroups(): boolean {
    const node: Value = this._jsonValue[FrequestNode.FrequestNode_Motions];
    return !node.isNull() && !node.isError();
  }

  /**
   * 引数で指定したモーショングループのキーが存在するかどうかを確認する
   * @param groupName  グループ名
   * @return true キーが存在する
   * @return false キーが存在しない
   */
  protected isExistMotionGroupName(groupName: string): boolean {
    const node: Value =
      this._jsonValue[FrequestNode.FrequestNode_Motions].getValueByString(
        groupName
      );
    return !node.isNull() && !node.isError();
  }

  /**
   * 引数で指定したモーションに対応するサウンドファイルのキーが存在するかどうかを確認する
   * @param groupName  グループ名
   * @param index 配列のインデックス値
   * @return true キーが存在する
   * @return false キーが存在しない
   */
  protected isExistMotionSoundFile(groupName: string, index: number): boolean {
    const node: Value = this._jsonValue[FrequestNode.FrequestNode_Motions]
      .getValueByString(groupName)
      .getValueByIndex(index)
      .getValueByString(this.soundPath);
    return !node.isNull() && !node.isError();
  }

  /**
   * 引数で指定したモーションに対応するフェードイン時間のキーが存在するかどうかを確認する
   * @param groupName  グループ名
   * @param index 配列のインデックス値
   * @return true キーが存在する
   * @return false キーが存在しない
   */
  protected isExistMotionFadeIn(groupName: string, index: number): boolean {
    const node: Value = this._jsonValue[FrequestNode.FrequestNode_Motions]
      .getValueByString(groupName)
      .getValueByIndex(index)
      .getValueByString(this.fadeInTime);
    return !node.isNull() && !node.isError();
  }

  /**
   * 引数で指定したモーションに対応するフェードアウト時間のキーが存在するかどうかを確認する
   * @param groupName  グループ名
   * @param index 配列のインデックス値
   * @return true キーが存在する
   * @return false キーが存在しない
   */
  protected isExistMotionFadeOut(groupName: string, index: number): boolean {
    const node: Value = this._jsonValue[FrequestNode.FrequestNode_Motions]
      .getValueByString(groupName)
      .getValueByIndex(index)
      .getValueByString(this.fadeOutTime);
    return !node.isNull() && !node.isError();
  }

  /**
   * UserDataのファイル名が存在するかどうかを確認する
   * @return true キーが存在する
   * @return false キーが存在しない
   */
  protected isExistUserDataFile(): boolean {
    const node: Value = this.getJson()
      .getRoot()
      .getValueByString(this.fileReferences)
      .getValueByString(this.userData);
    return !node.isNull() && !node.isError();
  }

  /**
   * 目ぱちに対応付けられたパラメータが存在するかどうかを確認する
   * @return true キーが存在する
   * @return false キーが存在しない
   */
  protected isExistEyeBlinkParameters(): boolean {
    if (
      this._jsonValue[FrequestNode.FrequestNode_Groups].isNull() ||
      this._jsonValue[FrequestNode.FrequestNode_Groups].isError()
    ) {
      return false;
    }

    for (
      let i = 0;
      i < this._jsonValue[FrequestNode.FrequestNode_Groups].getSize();
      ++i
    ) {
      if (
        this._jsonValue[FrequestNode.FrequestNode_Groups]
          .getValueByIndex(i)
          .getValueByString(this.name)
          .getRawString() == this.eyeBlink
      ) {
        return true;
      }
    }

    return false;
  }

  /**
   * リップシンクに対応付けられたパラメータが存在するかどうかを確認する
   * @return true キーが存在する
   * @return false キーが存在しない
   */
  protected isExistLipSyncParameters(): boolean {
    if (
      this._jsonValue[FrequestNode.FrequestNode_Groups].isNull() ||
      this._jsonValue[FrequestNode.FrequestNode_Groups].isError()
    ) {
      return false;
    }
    for (
      let i = 0;
      i < this._jsonValue[FrequestNode.FrequestNode_Groups].getSize();
      ++i
    ) {
      if (
        this._jsonValue[FrequestNode.FrequestNode_Groups]
          .getValueByIndex(i)
          .getValueByString(this.name)
          .getRawString() == this.lipSync
      ) {
        return true;
      }
    }
    return false;
  }

  protected _json: CubismJson;
  protected _jsonValue: Array<Value>;

  /**
   * Model3Jsonのキー文字列
   */
  protected readonly version = 'Version';
  protected readonly fileReferences = 'FileReferences';

  protected readonly groups = 'Groups';
  protected readonly layout = 'Layout';
  protected readonly hitAreas = 'HitAreas';

  protected readonly moc = 'Moc';
  protected readonly textures = 'Textures';
  protected readonly physics = 'Physics';
  protected readonly pose = 'Pose';
  protected readonly expressions = 'Expressions';
  protected readonly motions = 'Motions';

  protected readonly userData = 'UserData';
  protected readonly name = 'Name';
  protected readonly filePath = 'File';
  protected readonly id = 'Id';
  protected readonly ids = 'Ids';
  protected readonly target = 'Target';

  // Motions
  protected readonly idle = 'Idle';
  protected readonly tapBody = 'TapBody';
  protected readonly pinchIn = 'PinchIn';
  protected readonly pinchOut = 'PinchOut';
  protected readonly shake = 'Shake';
  protected readonly flickHead = 'FlickHead';
  protected readonly parameter = 'Parameter';

  protected readonly soundPath = 'Sound';
  protected readonly fadeInTime = 'FadeInTime';
  protected readonly fadeOutTime = 'FadeOutTime';

  // Layout
  protected readonly centerX = 'CenterX';
  protected readonly centerY = 'CenterY';
  protected readonly x = 'X';
  protected readonly y = 'Y';
  protected readonly width = 'Width';
  protected readonly height = 'Height';

  protected readonly lipSync = 'LipSync';
  protected readonly eyeBlink = 'EyeBlink';

  protected readonly initParameter = 'init_param';
  protected readonly initPartsVisible = 'init_parts_visible';
  protected readonly val = 'val';
}

// Namespace definition for compatibility.
import * as $ from './cubismmodelsettingjson';
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace Live2DCubismFramework {
  export const CubismModelSettingJson = $.CubismModelSettingJson;
  export type CubismModelSettingJson = $.CubismModelSettingJson;
  export const FrequestNode = $.FrequestNode;
  export type FrequestNode = $.FrequestNode;
}
