import { getModId } from "lazuli-core";
import { Container, Player, RawMessage } from "@minecraft/server";
import { ItemData, EntityData, DisplayCondition } from "lazuli-common";
import { getItemAmountInContainer, giveItem, modAlert } from "lazuli-utils";
import {
  ActionFormData,
  MessageFormData,
} from "@minecraft/server-ui";

/**
 * Namespace of Task Complete Tag.
 */
let taskNameSpace: string = getModId();

/**
 * Create a task.
 * @catetory Need Registry
 */
export class Task {
  constructor(
    /**
     * The unique id of the task.
     */
    readonly id: string,
    /**
     * Title of the task, only support {@link RawMessage}.
     */
    public _title: RawMessage,
    /**
     * Body of the task, only support {@link RawMessage}.
     */
    public _body: RawMessage,
    /**
     * Options of the task.
     *
     * *You MUST assign the complete condition and the award, others are optional.*
     */
    public options: TaskOptions
  ) {
    if (!this.options.unlock) {
      this.options.unlock = true;
    }
  }
  /**
   *
   * @param player
   * @param backTo The screen that player should return to after closing the form.
   */
  protected displayInfo(player: Player, backTo?: TaskList | TaskCategory) {
    const body = TaskUtils.generateBody(this.body, this.options);
    const form = new MessageFormData()
      .title(this.title)
      .body(body)
      .button1({ translate: "gui.back" });
    if (this.isCompleted(player)) {
      form.button2({ translate: "task.done" });
    } else {
      form.button2({ translate: "task.check" });
    }
    form.show(player).then((response) => {
      if (
        response.canceled ||
        response.selection === undefined ||
        response.selection === 0 ||
        this.isCompleted(player)
      ) {
        backTo?.display(player);
      } else if (response.selection === 1) {
        if (
          TaskUtils.checkCondition(this.options.condition, player, this).result
        ) {
          this.complete(player);
        } else {
          player.sendMessage(
            TaskUtils.checkCondition(this.options.condition, player, this)
              .message
          );
        }
      }
    });
  }
  /**
   *
   * @param player
   * @param backTo
   */
  protected displayFail(player: Player, backTo?: TaskList | TaskCategory) {
    const UNLOCK = this.options.unlock;
    modAlert({ translate: "task.displayFail.unlock" }, player);
    if (typeof UNLOCK === "object") {
      player.sendMessage(
        TaskUtils.checkCondition(UNLOCK, player, this).message
      );
    }
  }
  /**
   * Display ui form to the player.
   * @param player
   * @param backTo The screen that player should return to after closing the form.
   */
  display(player: Player, backTo?: TaskList | TaskCategory): void {
    if (this.isUnlocked(player)) {
      this.displayInfo(player, backTo);
    } else if (typeof this.options.unlock === "object") {
      if (TaskUtils.checkCondition(this.options.unlock, player, this)) {
        this.unlock(player);
        this.displayInfo(player, backTo);
      } else {
        this.displayFail(player, backTo);
      }
    } else {
      modAlert({ translate: "task.displayFail.noAccess" }, player);
    }
  }
  /**
   * Let a player complete the task and give award.
   * @param player
   */
  complete(player: Player): void {
    player.addTag(TaskUtils.getCompleteTag(this));
    player.addLevels(this.options.award.level ?? 0);
    player.addExperience(this.options.award.exp ?? 0);
    if (this.options.award?.item?.itemStack) {
      giveItem(player, this.options.award?.item?.itemStack);
    }
    if (this.options.award.custom) {
      this.options.award.custom(player);
    }
    player.playSound("random.levelup");
    player.sendMessage({
      translate: "task.finished",
    });
  }
  /**
   * Let a player complete the task and give award.
   * @param player
   */
  unlock(player: Player): void {
    player.addTag(TaskUtils.getUnlockTag(this));
    player.onScreenDisplay.setActionBar({
      translate: "task.unlock",
    });
  }
  /**
   * Check if the player has the complete tag.
   * @param player
   */
  isCompleted(player: Player): boolean {
    return player.hasTag(TaskUtils.getCompleteTag(this));
  }
  /**
   * Check if the player has the unlock tag.
   * @param player
   */
  isUnlocked(player: Player): boolean {
    const UNLOCK = this.options.unlock;
    if (typeof UNLOCK === "boolean") {
      return UNLOCK;
    } else {
      return player.hasTag(TaskUtils.getUnlockTag(this));
    }
  }
  set body(content: RawMessage) {
    this._body = content;
  }
  set title(content: RawMessage) {
    this._title = content;
  }
  get body() {
    return this._body;
  }
  get title() {
    return this._title;
  }
}

export class TaskCategory {
  constructor(
    public title: RawMessage,
    public body: RawMessage,
    public items: Task[],
    public iconPath?: string
  ) {}
  /**
   * Display ui form to the player.
   * @param player
   * @param backTo The screen that player should return to after closing the form.
   */
  display(player: Player, backTo?: TaskList): void {
    const form = new ActionFormData().title(this.title).body(this.body);
    this.items.forEach((task) => {
      let rawTitle: RawMessage = { rawtext: [this.title] };
      if (task.isCompleted(player)) {
        rawTitle.rawtext?.push({ text: " §2✔" });
      }
      form.button(rawTitle, task.options.iconPath);
    });
    form.show(player).then((response) => {
      if (response.canceled || response.selection === undefined) {
        backTo?.display(player);
      } else {
        this.items[response.selection].display(player);
      }
    });
  }
  /**
   * Returns whether the player has completed all tasks in the category.
   * @param player
   * @returns Whether the player has completed all tasks in the category.
   */
  isCompleted(player: Player): boolean {
    let value = this.items.every((task) => {
      return task.isCompleted(player) === true;
    });
    return value;
  }
}

export class TaskList {
  constructor(
    readonly id: string,
    public title: RawMessage,
    public body: RawMessage,
    public options: TaskListOptions
  ) {}
  display(player: Player) {
    const form = new ActionFormData().title(this.title).body(this.body);
    this.options.items.forEach((item) => {
      let rawTitle: RawMessage = { rawtext: [this.title] };
      if (item.isCompleted(player)) {
        rawTitle.rawtext?.push({ text: " §2✔" });
      }
      form.button(rawTitle);
    });
    form.show(player).then(response=>{
      if (response.canceled || response.selection === undefined) {
        return;
      }
      this.options.items[response.selection].display(player);
    })
  }
}

export class TaskUtils {
  /**
   * Get a task's complete tag.
   * @param task
   * @return The task's complete tag.
   */
  static getCompleteTag(task: Task) {
    return `${taskNameSpace}:${task.id}`;
  }
  /**
   * Get a task's complete tag.
   * @param task
   * @return The task's complete tag.
   */
  static getUnlockTag(task: Task) {
    return `${taskNameSpace}:${task.id}.unlock`;
  }
  /**
   * Set name space of the task complete tag.
   * @param str
   */
  static setNameSpace(str: string) {
    taskNameSpace = str;
  }
  static checkCondition(
    options: TaskUnlockCompleteOptions,
    player: Player,
    task: Task
  ): checkResult {
    let message: RawMessage = { rawtext: [] };
    const CONTAINER: undefined | Container = player.getComponent(
      "minecraft:inventory"
    )?.container;
    if (!CONTAINER) {
      throw new Error("Task Api unable to access player's Container!");
    }
    if (
      options.item &&
      getItemAmountInContainer(CONTAINER, options.item.itemStack.typeId) <
        options.item.itemStack.amount
    ) {
      message.rawtext?.push({
        rawtext: [
          {
            translate: "task.not_enough.item",
            with: {
              rawtext: [
                {
                  text: options.item.itemStack.amount.toString(),
                },
                options.item.name,
              ],
            },
          }, // Following items are missing from your inventory:
          {
            text: "\n",
          },
        ],
      });
    }
    if (options.level && player.level < options.level) {
      message.rawtext?.push({
        rawtext: [
          {
            translate: "task.not_enough.level",
            with: [player.level.toString()],
          }, // You need %%1 more level(s)!
          {
            text: "\n",
          },
        ],
      });
    }
    if (options.exp && player.getTotalXp() < options.exp) {
      message.rawtext?.push({
        rawtext: [
          {
            translate: "task.not_enough.xp",
            with: [player.getTotalXp.toString()],
          }, // You need %%1 more experience!
          {
            text: "\n",
          },
        ],
      });
    }
    if (options.tasks) {
      let finishList: boolean[] = [];
      let taskList: RawMessage = {
        rawtext: [],
      };
      options.tasks.forEach((task) => {
        finishList.push(task.isCompleted(player));
        taskList.rawtext?.push(task.title);
      });
      if (
        !finishList.every((value) => {
          return value === true;
        })
      ) {
        message.rawtext?.push({
          rawtext: [
            {
              translate: "task.not_enough.tasks",
              with: taskList,
            }, // You need %%1 more experience!
            {
              text: "\n",
            },
          ],
        });
      }
    }
    if (!task.isCompleted(player) && options.killEntity) {
      message.rawtext?.push({
        rawtext: [
          {
            translate: "task.not_enough.entity",
            with: [options.killEntity.name],
          },
        ],
      });
    }
    return {
      result: <number>message.rawtext?.length === 0,
      message: message,
    };
  }
  /**
   * Generate the body of Task, includes descriptions, conditions, awards and tips.
   * @param description
   * @param options
   * @return A RawMessage that includes task's descriptions, conditions, awards and tips.
   */
  static generateBody(description: RawMessage, options: TaskOptions) {
    const [CONDITION, AWARD] = [options.condition, options.award];
    let body: RawMessage = {
      rawtext: [
        description,
        { text: "\n\n" },
        { translate: "task.condition.title" },
      ],
    };
    if (Object.getOwnPropertyNames(CONDITION).length === 0) {
      body.rawtext?.push({
        translate: "task.condition.none",
      });
    } else {
      if (CONDITION.item) {
        body.rawtext?.push({
          translate: "task.item",
          with: {
            rawtext: [
              {
                text: CONDITION.item.itemStack.amount.toString(),
              },
              CONDITION.item.name,
            ],
          },
        });
      }
      if (CONDITION.exp) {
        body.rawtext?.push({
          translate: "task.exp",
          with: [CONDITION.exp.toString()],
        });
      }
      if (CONDITION.level) {
        body.rawtext?.push({
          translate: "task.level",
          with: [CONDITION.level.toString()],
        });
      }
      if (CONDITION.killEntity) {
        body.rawtext?.push({
          translate: "task.entity",
          with: [CONDITION.killEntity.name],
        });
      }
      if (CONDITION.tasks) {
        let taskList: RawMessage = {
          rawtext: [],
        };
        CONDITION.tasks.forEach((task) => {
          taskList.rawtext?.push(task._title);
        });
        body.rawtext?.push({
          translate: "task.tasks",
          with: taskList,
        });
      }
    }
    body.rawtext?.push({ text: "\n" }, { translate: "task.award.title" });
    if (Object.getOwnPropertyNames(AWARD).length === 0) {
      body.rawtext?.push({
        translate: "task.award.none",
      });
    } else {
      if (AWARD.item) {
        body.rawtext?.push({
          translate: "task.item",
          with: {
            rawtext: [
              {
                text: AWARD.item.itemStack.amount.toString(),
              },
              AWARD.item.name,
            ],
          },
        });
      }
      if (AWARD.exp) {
        body.rawtext?.push({
          translate: "task.exp",
          with: [AWARD.exp.toString()],
        });
      }
      if (AWARD.level) {
        body.rawtext?.push({
          translate: "task.level",
          with: [AWARD.level.toString()],
        });
      }
    }
    if (options.tips) {
      body.rawtext?.push(
        { text: "\n" },
        { translate: "task.tips.title" },
        options.tips
      );
    }
    return body;
  }
}

/**
 * Contidions that control when unlock or complete the task.
 *
 * **NOTE: If specify more than one condition, the task will be unlocked when any condition is achieved, but it will be completed after all conditions are achieved.**
 */
export interface TaskUnlockCompleteOptions {
  /**
   * Unlock the task when player has the specific item(s).
   */
  item?: ItemData;
  /**
   * The specific entity will be required to unlock the task.
   */
  killEntity?: EntityData;
  /**
   * The specific level will be required to unlock the task.
   */
  level?: number;
  /**
   * The specific point will be required to unlock the task.
   */
  exp?: number;
  /**
   * The specific tasks will be required to unlock the task.
   */
  tasks?: Task[];
}

/**
 * Award of the task.
 */
export interface TaskAward {
  /**
   * Player will get these items when the task is finished.
   */
  item?: ItemData;
  /**
   * The specific level will be given to the player.
   */
  level?: number;
  /**
   * The specific point will be given to the player.
   */
  exp?: number;
  /**
   * Custom award.
   * @param player
   */
  custom?: (player: Player) => void;
}

export interface TaskOptions {
  condition: TaskUnlockCompleteOptions;
  award: TaskAward;
  unlock?: TaskUnlockCompleteOptions | boolean;
  tips?: RawMessage;
  iconPath?: string;
}

export interface TaskListOptions {
  items: (Task | TaskCategory)[];
  displayCondition: DisplayCondition;
  iconPath?: string;
}

export interface checkResult {
  result: boolean;
  message: RawMessage;
}
