/* 
  Npc 们的公共继承类
  决定了普通NPC和特殊NPC的原型
*/

import AStarManager from "../AStar/AStarManager";
import ResLoaderManager from "../Manager/ResLoaderManager";
import NpcAniManager from "./NpcAniManager";

const { ccclass, property } = cc._decorator;

export interface Coordinate {
  x: number,
  y: number,
  direction?: string
}

// 定义位置 常量
enum Posi {
  TOP, RIGHT, BOTTOM, LEFT
}

export enum NpcState {
  VISIT,        // 参观
  WALK,         // 行走
  FREE,         // 闲逛
  LEAVE,        // 离开
  INTERACTIVE,  // 互动 特殊NPC独有 当达到一定好感度时触发
}

// NPC 的心情常量
export enum Mood {
  AMAZEMENT,        // 惊愕
  ANGER,            // 愤怒
  CHERISH,          // 珍惜
  CRY,              // 哭
  CRYING,           // 大哭
  FIGHTING,         // 奋斗
  LIKE,             // 喜欢
  QUERY,            // 疑问
  SMILE,            // 微笑
  SUN_SMILE,        // 阳关笑容
  SWEAT,            // 无语
}

@ccclass
export default class NpcSupper extends cc.Component {

  /*********特殊属性 决定npc的核心*********/
  // 当前npc在配置表中的ID 方便以后的操作
  npcId: number = null
  // 性别
  sex: string = null
  // 年龄
  age: number = null
  // 口头禅
  phraseList: string = null
  // 图集资源地址 每个NPC都有一个独一无二的标识地址, 程序会根据这个标识地址去添加固定字符串然后寻找特定图片
  source: string = null
  // npc名称
  npcName: string = null
  /*********特殊属性 决定npc的核心*********/

  // 墙的图层 用于做 坐标处理
  @property(cc.TiledLayer)
  wallLayer: cc.TiledLayer = null

  /** 透明坐标图层*/
  @property(cc.TiledLayer)
  moveLayer: cc.TiledLayer = null

  // npc 节点
  @property(cc.Node)
  npcNode: cc.Node = null

  // npc 节点上的动画组件
  @property(cc.Animation)
  npcAnim: cc.Animation = null

  // npc节点上的文字节点
  @property(cc.Node)
  npcStateWrap: cc.Node = null

  // 描述NPC 状态信息的表情节点
  @property(cc.Node)
  npcMoodNode: cc.Node = null

  // 描述NPC 状态信息的表情Sprite组件
  @property(cc.Sprite)
  npcMood: cc.Sprite = null

  // 资源管理的实例
  sm: ResLoaderManager = null

  // 移动的下标
  moveIndex: number = null
  // 移动的数组
  moveList: Array<any> = null

  // 当前npc所在的 x，y坐标
  coordinate: Coordinate = null

  // 外界要求前往的目标点
  targetCoordinate: { x: number, y: number, direction?: string } = null

  // 是否在参观展品还是在行走, 需要一个状态 
  state: NpcState = null

  // npc 当前移动的方向
  movePosi: Posi = null

  // 已经参观过多少个展品
  visited: number = null

  // 期待参观数 当已参观数量大于等于 期待参观数时，就通知外界， 这个npc想要离开
  visit: number = null

  // 获取A* 管理器的实例
  aStarInstance: AStarManager = null

  // npc 的移动速度
  ispeed: number = null

  // npc 的移动动画执行速度
  walkIspeed: number = null

  // 所有NPC的动画配置对象
  npcAnimConfig: any = null

  // npc的标志
  sign: string = null

  // 初始化数据
  init() {
    // 获取表情节点
    this.npcMoodNode = this.node.getChildByName('look')
    // 获取表情节点的 Sprite 组件
    this.npcMood = this.npcMoodNode.getChildByName('icon').getComponent(cc.Sprite)
    // 获取文字容器
    this.npcStateWrap = this.node.getChildByName('stateWrap')

    // 初始化npc节点
    this.npcNode = cc.find('img_npc', this.node)
    // 获取动画组件
    this.npcAnim = this.npcNode.getComponent(cc.Animation)

    // 初始化npc的速度 最快0.5 - 1 保留2位有限小数
    let offset = +(Math.random() * 0.3 + 0.2).toFixed(2) // 一点小偏移
    this.ispeed = 1 - offset // 这个是移动一段距离的时间
    // npc 的移动动画执行速度
    this.walkIspeed = 1 + offset

    // 获取墙的图层
    let map = cc.find('Canvas/Baseview/obj_map/map100').getComponent(cc.TiledMap)
    this.wallLayer = map.getLayer('wall')
    this.moveLayer = map.getLayer('move')

    // 初始化已参观数
    this.visited = 0
    // 初始化期待参观数
    this.visit = Number.MAX_VALUE

    // 初始化A*实例
    this.aStarInstance = AStarManager.getInstance()

    // 初始化资源管理的实例
    let sm = this.sm = ResLoaderManager.getInstance()

    // 读取配置表
    this.npcAnimConfig = sm.getConfig('animation')

  }

  // 更改当前移动方向
  changeMovePosi(state: Posi) {
    this.movePosi = state
  }

  // 更改当前NPC状态
  changeState(state: NpcState) {
    this.state = state
  }

  // 转向 将原先4个函数 合并成一个函数
  turnACorner(direction: Posi, scaleX: number, animName: string) {
    // 避免重复触发
    if (this.movePosi === direction) return

    // 改变行进方向
    this.changeMovePosi(direction)

    // 镜像翻转节点
    this.npcNode.scaleX = scaleX
    // 播放动画
    this.npcAnim.play(animName)
  }

  // 从当前点, 移动到某一点
  moveTo(target: Coordinate) {
    // 当前位置信息
    let { coordinate } = this

    // 坐标点比对
    if (coordinate.x - target.x > 0) {
      // 向左
      this.turnACorner(Posi.LEFT, 1, 'npc1_rear')
    } else if (coordinate.x - target.x < 0) {
      // 向右
      this.turnACorner(Posi.RIGHT, -1, 'npc1_front')
    } else if (coordinate.y - target.y > 0) { // 因为当前程序不走斜角， 所以可以连着esle
      // 向前
      this.turnACorner(Posi.TOP, -1, 'npc1_rear')
    } else if (coordinate.y - target.y < 0) {
      // 向后
      this.turnACorner(Posi.BOTTOM, 1, 'npc1_front')
    }

    // 切换坐标
    this.coordinate = target

    // 判断是否透明点
    if (this.node.opacity !== 0) {
      this.moveLayer.getTileGIDAt(target.x, target.y) != 0 ? this.node.opacity = 127.5 : this.node.opacity = 255
    }

    // 计算坐标位置
    let v2 = this.correctCoordinate(target)
    // 前往
    cc.tween(this.node)
      .to(this.ispeed, { position: v2 })
      .call(() => {
        // 因为需要多次访问 提前存下
        let len = this.moveList.length

        // 移动未结束
        if (this.moveIndex < len - 1) {
          // 可以进行下一次移动
          this.moveTo(this.moveList[++this.moveIndex])
        } else {

          // 移动结束 进入分支 开始执行不同的程序
          this.shunt()
        }

        // 在这里处理 当moveIndex 移动到倒数第二个格子的时候, 同时是离开的时候淡出
        if (this.moveIndex === len - 2 && this.visit === this.visited) {
          // 淡出
          this.fadeOut()
        }

      })
      .start()
  }

  // 由管理器决定终点位置
  go(posi: Coordinate) {
    // 每次管理器调用这个函数时, 不管当前终点有无抵达, 一样切换行进坐标点 同时更改数组下标
    let moveList = this.aStarInstance.findPath(this.coordinate, posi)

    // 容错判断 如果没有查找到可行路径
    if (moveList === null) {
      // 先退出当前寻路
      // 丢弃当前点，直接进入下一次
      return false
    }

    this.moveList = moveList
    // 下标归零
    this.moveIndex = 0

    // 决定目标点
    this.targetCoordinate = posi

    // 如果npc 当前还在行走中, 你就切换终点, 那么就直接return 程序会自己接上去的
    if (this.state != NpcState.WALK) {
      // 迭代器启动
      this.moveTo(this.moveList[++this.moveIndex])
    }

    // npc 状态进入 行走
    this.changeState(NpcState.WALK)

    // 路径可行
    return true
  }

  // 分流 当moveTo抵达终点后, 有三种情况需要处理
  shunt() {
    // 1 抵达是展台 那么进入参观阶段
    // 2 抵达的是闲逛点 那么进入等待阶段

    // 判断终点是否是展台并且没有参观完成 -1 是因为如果函数进入这里
    if (this.targetCoordinate.direction && this.visited < this.visit) {
      // npc需要去参观当前展台
      this.lookAround()

      // 是否未抵达参观数量要求
      if (this.visited < this.visit) {
        // 这个函数中有延迟器 逗留一段时间后 通知外界 npc需要前往下一个参观点
        this.delayGoNext(this.produce) // 传入的参数是一个函数， 这个函数是用于制作产出
      } else if (this.visited === this.visit) {
        // 该离开这里令人怀念的地方了
        this.leval()
      }

    } else if (!this.targetCoordinate.direction && this.visited < this.visit) {
      // 当前npc没有参观完成展台, 并且终点不是站台位置 说明处于闲逛中
      // 等待
      this.wait()
      // 我要前往下一个参观点
      this.delayGoNext()

    } else if (this.visited === this.visit) { // 因为每当npc移动到一个点后都会触发这个函数
      // 销毁阶段 
      this.node.removeFromParent()
    }

  }

  // NPC 处于等待状态 这只是个动画的切换, 它会根据你当前的方位执行等待动画
  wait() {
    // 切换NPC状态
    this.changeState(NpcState.FREE)

    // 移动结束 移动方向置为空
    this.movePosi = null

    Math.random() > 0.5 ? this.npcAnim.play('stand_b') : this.npcAnim.play('action_b')
    // 镜像翻转
    this.npcNode.scaleX = 1
  }

  // 参观
  lookAround() {
    // 更改npc状态进入参观
    this.changeState(NpcState.VISIT)

    // 根据当前展台朝向 定义人物位置 依据的是展台朝向
    switch (this.targetCoordinate.direction) {
      case 'top':
        Math.random() > 0.5 ? this.npcAnim.play('stand_a') : this.npcAnim.play('action_a')
        // 镜像翻转
        this.npcNode.scaleX = -1
        break
      case 'right':
        Math.random() > 0.5 ? this.npcAnim.play('stand_b') : this.npcAnim.play('action_b')
        // 镜像翻转
        this.npcNode.scaleX = -1
        break
      case 'bottom':
        Math.random() > 0.5 ? this.npcAnim.play('stand_b') : this.npcAnim.play('action_b')
        // 镜像翻转
        this.npcNode.scaleX = 1
        break
      case 'left':
        Math.random() > 0.5 ? this.npcAnim.play('stand_a') : this.npcAnim.play('action_a')
        // 镜像翻转
        this.npcNode.scaleX = 1
        break
    }

    // 移动结束 移动方向置为空
    this.movePosi = null

    // 参观完成 参观数 ++
    this.visited++
  }

  // 淡入
  fadeIn(cb: Function = () => { }) {
    // 新版API
    cc.tween(this.node)
      .to(.5, { opacity: 255 })
      .call(cb.bind(this))
      .start()
  }

  // 淡出
  fadeOut(cb: Function = () => { }) {
    // 
    cc.tween(this.node)
      .to(.5, { opacity: 0 })
      .call(cb.bind(this))
      .start()
  }

  // 离场
  leval() {
    // 通知外界 我要离开
    this.scheduleOnce(() => {
      // 当参观展品数达到数量 那么就直接告诉外界, 我要离开
      let event = new cc.Event.EventCustom('leave', false)
      // 设置参数
      event.setUserData(this)
      // 发送事件
      cc.systemEvent.dispatchEvent(event)

      // 最后一次金币生成
      this.produce()

      // 更改状态
      this.changeState(NpcState.LEAVE)
    }, 3)
  }

  // 对goNext进行一个延迟
  delayGoNext(func?: Function) {
    // 延迟一段时间
    this.scheduleOnce(() => {

      // goNext 执行
      this.goNext(func)

    }, Math.random() * 5 + 5)
    // 停留时间是 最低5秒 最长9.9秒 Math.random() * 5 + 5
  }

  // 前往下一个逗留点
  goNext(func?: Function) {

    // 这个一般是等npc离开后产出金币用的
    func && func.call(this)

    // 参观一段时间通知管理， 当前npc 希望前往另一个参观点
    let event = new cc.Event.EventCustom('visitfinish', false)
    // 设置参数
    event.setUserData(this)
    // 发射
    cc.systemEvent.dispatchEvent(event)

  }

  // 计算并修正坐标
  correctCoordinate({ x, y }) {
    ({ x, y } = this.wallLayer.getPositionAt(x, y))
    // 因为 坐标转换有些偏差， 需要矫正这些偏差.0
    return cc.v2(x + 32, y + 16)
  }

  // 根据管理器要求 设置出生点
  setBirthPoint(posi: Coordinate) {
    // 赋值当前点坐标
    this.coordinate = posi

    // 计算坐标 并赋值
    this.node.setPosition(this.correctCoordinate(posi))
  }

  // 外界调用 注入模板 暂时不要进行内部自己随机
  soulInit(config) {
    // 需要初始化的信息包括 npc的外观，npc的口头禅， npc的年龄， npc的性别等类人信息
    this.phraseList = config.visitor_phrase   // 口头禅 是数组， 从中随机
    this.source = config.visitor_resources    // npc图集地址

    // 避免重复访问
    let npcAnim = this.npcAnim
    let animId = config.animation_id

    // 查询一次 如果一个没有，说明其他也没有
    let walkA1 = NpcAniManager.getAnimClip(animId, 'npc1_front')

    if (walkA1) { // 存在的情况
      // 更改动画速度
      let walkB1 = NpcAniManager.getAnimClip(animId, 'npc1_rear')

      walkA1.speed = this.walkIspeed
      walkB1.speed = this.walkIspeed

      // 添加动画帧
      npcAnim.addClip(walkA1)
      npcAnim.addClip(walkB1)
      npcAnim.addClip(NpcAniManager.getAnimClip(animId, 'stand_a'))
      npcAnim.addClip(NpcAniManager.getAnimClip(animId, 'stand_b'))
      npcAnim.addClip(NpcAniManager.getAnimClip(animId, 'action_a'))
      npcAnim.addClip(NpcAniManager.getAnimClip(animId, 'action_b'))

    } else { // 没有的情况
      // 拿到对应的动画配置表
      let animConfig = this.npcAnimConfig[animId]

      // 动作集
      let { walk_a: walkA, walk_b: walkB, stand_b_1: standA, stand_a_1: standB, stand_b_2: actionA, stand_a_2: actionB } = animConfig

      // 获取图集资源 TEST
      let atlas = this.sm.getSpriteAtlasByName(this.source.split('/')[1])

      // 创建动画帧并添加
      npcAnim.addClip(NpcAniManager.createClip(animId, NpcAniManager.createFrames(walkA, atlas), 'npc1_front', this.walkIspeed))
      npcAnim.addClip(NpcAniManager.createClip(animId, NpcAniManager.createFrames(walkB, atlas), 'npc1_rear', this.walkIspeed))
      npcAnim.addClip(NpcAniManager.createClip(animId, NpcAniManager.createFrames(standA, atlas), 'stand_a', animConfig.stand_a_1_gap))
      npcAnim.addClip(NpcAniManager.createClip(animId, NpcAniManager.createFrames(standB, atlas), 'stand_b', animConfig.stand_a_1_gap))
      npcAnim.addClip(NpcAniManager.createClip(animId, NpcAniManager.createFrames(actionA, atlas), 'action_a', animConfig.stand_a_2_gap))
      npcAnim.addClip(NpcAniManager.createClip(animId, NpcAniManager.createFrames(actionB, atlas), 'action_b', animConfig.stand_a_2_gap))
    }

    // UI 渲染 过1秒钟左右后再获取最新的高度
    this.scheduleOnce(() => {
      this.initEnd()
    }, 1);
  }

  // 当所有同步和异步操作都执行完成 执行这里的代码， 一般都是UI处理
  initEnd() {
    // 处理节点高度问题 每个NPC高度都不一致 
    this.npcMoodNode.y = this.npcNode.height + 5
    this.npcStateWrap.y = this.npcNode.height + 10
  }

  // 产出人气
  produce() {

  }

  onLoad() {
    // 设置当前节点透明
    this.node.opacity = 0
    // 初始化中
    this.init()

    // init 完成后 执行淡入
    this.fadeIn()
  }

  // start() { }

  // update() { }
}

/*
  A* 优化
  寻找离终点最近的中转点
  判断是否离npc过远， 如果过远，再以当前中转点为距离
  寻找离当前点最近的中转点

  13.2 14.25 10.9 15.9 14.7
  68.95 / 5
  13.790000000000001

  18.0 20.1 20.6 16.5 17.8
  93 / 5
  18.6

  18.5 16.4 20.4 19.6 20.0
  94.9 / 5
  18.98

  对话处理
    相同特点 -> 框会根据字体动态拉伸大小， 而且是动画形式

    如何确定框的大小

    大声说话 -> 字体变大
    长句陈述 ->
    内心想法

*/