import { ContextI } from "../ContextI"
import { segments } from "../shared/segments"
import { StateI } from "./StateI"
import * as PIXI from 'pixi.js'
import * as planck from 'planck'
import { DecorationInstance } from "../shared/decors"
import { SpriteInstance, sprites } from "../shared/sprites"
import { ShapeCircle, ShapePoints } from "../data/sprite"
import { SegmentZone } from "../data/segment"

interface Action {
  type: string
  priority: number
}

class Zone {
  fixture: planck.Fixture|undefined
  type: 'solid'|'fluid'
  points: [number, number][]
  constructor(z: SegmentZone) {
    this.type = z.type
    this.points = z.points.map(v=>[v[0], v[1]])
  }
}

class Entity {
  sprite: SpriteInstance
  body?: planck.Body
  velocity: [number, number] = [0, 0]
  acceleration: number = 0.5
  maxSpeed: number = 4
  direction: number = 0
  turnRate: number = 10

  constructor(ctor: string) {
    this.sprite = new SpriteInstance(ctor)
  }

  update(delta: number) {
    // TODO: Update sprite.
  }

  get position(): [number, number] {
    if (this.body) {
      let p = this.body.getPosition()
      return [p.x, p.y]
    }
    return [this.sprite.container.x, this.sprite.container.y]
  }
  get x(): number {
    if (this.body) {
      let p = this.body.getPosition()
      return p.x
    }
    return this.sprite.container.x
  }
  set x(v: number) {
    if (this.body) {
      let p = this.body.getPosition()
      this.body.setPosition(
        planck.Vec2(
          v,
          p.y,
        )
      )
    } else {
      this.sprite.container.y = v
    }
  }
  get y(): number {
    if (this.body) {
      let p = this.body.getPosition()
      return p.y
    }
    return this.sprite.container.y
  }
  set y(v: number) {
    if (this.body) {
      let p = this.body.getPosition()
      this.body.setPosition(
        planck.Vec2(
          p.x,
          v,
        )
      )
    } else {
      this.sprite.container.y = v
    }
  }
}

class PlayerEntity extends Entity {
  action?: Action
  constructor(ctor: string) {
    super(ctor)
  }
  act(actions: Action[]) {
    this.action = actions.sort((a, b) => {
      if (a.priority < b.priority) {
        return -1
      }
      if (a.priority > b.priority) {
        return 1
      }
      return 0
    })[0]
  }
  update(delta: number) {
    if (this.action) {
      // FIXME: Use physics.
      let shouldMove = false
      switch(this.action.type) {
        case 'west':
          if (this.direction !== 0) {
            if (this.direction < 180) {
              this.direction -= this.turnRate
            } else {
              this.direction += this.turnRate
            }
          }
          shouldMove = true
          break
        case 'east':
          if (this.direction !== 180) {
            if (this.direction < 180) {
              this.direction += this.turnRate
            } else {
              this.direction -= this.turnRate
            }
          }
          shouldMove = true
          break
        case 'north':
          if (this.direction !== 90) {
            if (this.direction < 90 || this.direction > 270) {
              this.direction += this.turnRate
            } else {
              this.direction -= this.turnRate
            }
          }
          shouldMove = true
          break
        case 'south':
          if (this.direction !== 270) {
            if (this.direction > 90 && this.direction < 270) {
              this.direction += this.turnRate
            } else {
              this.direction -= this.turnRate
            }
          }
          shouldMove = true
          break
      }
      if (this.direction > 360) {
        this.direction = 0
      } else if (this.direction < 0) {
        this.direction = 360
      }
      if (shouldMove) {
        let r = this.direction * (Math.PI/180)
        if (Math.abs(this.velocity[0]) < this.maxSpeed) {
          this.velocity[0] -= Math.cos(r) * this.acceleration
        }
        if (Math.abs(this.velocity[1]) < this.maxSpeed) {
          this.velocity[1] -= Math.sin(r) * this.acceleration
        }
        let cardinal = this.getCardinal()
        if (this.sprite.subsetKey !== cardinal) {
          this.sprite.setCtor(`${this.sprite.spriteKey}.${this.sprite.animationKey}.${this.sprite.setKey}.${cardinal}.${this.sprite.frameIndex}`)
        }
      }
    }

    //
    this.velocity[0] *= 0.5
    this.velocity[1] *= 0.5

    // Eh... let's manually handle velocity
    this.body?.setLinearVelocity(planck.Vec2(this.velocity[0], this.velocity[1]))
  }
  getCardinal(): string {
    const degreesPerDirection = 360 / 8
    const angle = this.direction + degreesPerDirection / 2

    if (angle >= 0 * degreesPerDirection && angle < 1 * degreesPerDirection) {
      return 'w'
    } else if (angle >= 1 * degreesPerDirection && angle < 2 * degreesPerDirection) {
      return 'nw'
    } else if (angle >= 2 * degreesPerDirection && angle < 3 * degreesPerDirection) {
      return 'n'
    } else if (angle >= 3 * degreesPerDirection && angle < 4 * degreesPerDirection) {
      return 'ne'
    } else if (angle >= 4 * degreesPerDirection && angle < 5 * degreesPerDirection) {
      return 'e'
    } else if (angle >= 5 * degreesPerDirection && angle < 6 * degreesPerDirection) {
      return 'se'
    } else if (angle >= 6 * degreesPerDirection && angle < 7 * degreesPerDirection) {
      return 's'
    }
    return 'sw'
  }
}
function isPlayerEntity(o: any): o is PlayerEntity {
  return o.act
}

export function GameState(ctx: ContextI): StateI {
  let world: planck.World = planck.World({
    gravity: planck.Vec2(0, 0),
  })
  let worldBody: planck.Body = world.createBody({
    type: 'static',
  })

  let rootContainer = new PIXI.Container()
  let decorations: DecorationInstance[] = []
  let entities: Entity[] = []
  let zones: Zone[] = []

  let enter = () => {
    hookKeyboard()
    // Load the world segment.
    let w = segments.world
    if (!w) return ctx.pop()
    rootContainer.width = w.width
    rootContainer.height = w.height
    rootContainer.scale.set(2, 2)
    for (let l of w.layers) {
      let container = new PIXI.Container()
      container.width = w.width
      container.height = w.height

      for (let d of l.decorations) {
        let di = new DecorationInstance(d.decor, d.decoration)
        di.elapsed = d.timeOffset
        di.container.x = d.x
        di.container.y = d.y
        if (d.rotation !== 0) {
          di.container.angle = d.rotation
        }
        if (d.flip) {
          di.container.pivot.y = 1
          di.container.scale.y *= -1
          if (d.rotation === 0) {
            di.container.position.y--
          }
        }
        if (d.mirror) {
          di.container.pivot.x = 1
          di.container.scale.x *= -1
          di.container.position.x--
          if (d.rotation !== 0) {
            di.container.angle = -d.rotation
            di.container.position.x++
            di.container.position.y--
          }
        }
        container.addChild(di.container)
        decorations.push(di)
      }

      rootContainer.addChild(container)
    }

    for (let z of w.zones) {
      addZone(new Zone(z))
    }

    // Add bogus entity
    //addEntity(new PlayerEntity('animals.deer.animal.west.0'), 0, 0)
    addEntity(new PlayerEntity('bogus-arrows.player.normal.w.0'), 0, 0)

    ctx.app.stage.addChild(rootContainer)
  }
  let leave = () => {
    unhookKeyboard()
    for (let entity of entities) {
      removeEntity(entity)
    }
    ctx.app.stage.removeChild(rootContainer)
  }
  let elapsed: number = 0
  let update = (delta: number) => {
    elapsed += delta
    // Run world sim.
    while (elapsed >= 1/60) {
      world.step(1/60)
      elapsed -= 1/60
    }
    // Update/render.
    for (let decoration of decorations) {
      decoration.update(delta)
    }
    for (let entity of entities) {
      if (isPlayerEntity(entity)) {
        entity.act(desiredActions)
        // FIXME: This isn't the right place for this.
        rootContainer.position.set(Math.round(ctx.app.renderer.width/2), Math.round(ctx.app.renderer.height/2))
        rootContainer.pivot.set(Math.max(rootContainer.width/6, Math.min(entity.x, rootContainer.width/3)), Math.max(rootContainer.height/6, Math.min(entity.y, rootContainer.height/3)))
      }
      entity.update(delta)
      // I guess...
      entity.sprite.container.x = entity.x
      entity.sprite.container.y = entity.y
    }
  }

  let addEntity = (entity: Entity, x: number, y: number) => {
    if (entities.find(v=>v===entity)) return
    entities.push(entity)
    rootContainer.addChild(entity.sprite.container)
    // I guess this is a fair enough place to create physics and add it to the entity.
    let spriteShape = entity.sprite.getBodyShape()
    if (spriteShape) {
      let shape: planck.Shape|undefined
      if (spriteShape instanceof ShapeCircle) {
        shape = planck.Circle(planck.Vec2(spriteShape.x, spriteShape.y), spriteShape.radius)
      } else if (spriteShape instanceof ShapePoints) {
        shape = planck.Polygon(spriteShape.points.map(v=>planck.Vec2(v[0], v[1])))
      }
      if (shape !== undefined) {
        let body = world.createDynamicBody({
          position: planck.Vec2(entity.x, entity.y),
          fixedRotation: true,
        })
        let fixture = body.createFixture({
          shape,
          density: 1,
          friction: 0.9,
          restitution: 0.05,
        })
        entity.body = body
        body.setUserData(entity)
      }
    }
    entity.x = x
    entity.y = y
  }
  let removeEntity = (entity: Entity) => {
    entities = entities.filter(v=>v!==entity)
    rootContainer.removeChild(entity.sprite.container)
    if (entity.body) {
      world.destroyBody(entity.body)
      entity.body = undefined
    }
  }

  // Zonage
  let addZone = (zone: Zone) => {
    if (zones.find(v=>v===zone)) return
    let shape = planck.Chain(zone.points.map(v=>planck.Vec2(v[0],v[1])))
    zone.fixture = worldBody.createFixture({
      shape: shape,
    })
    if (zone.type === 'fluid') {
      zone.fixture.setSensor(true)
    }
  }
  let removeZone = (zone: Zone) => {
    zones = zones.filter(v=>v!==zone)
    if (zone.fixture) {
      worldBody.destroyFixture(zone.fixture)
    }
  }

  let desiredActions: Action[] = []
  let adjustAction = (type: string, v: number) => {
    let action = desiredActions.find(v=>v.type===type)
    if (!action) {
      desiredActions.push({
        type: type,
        priority: v,
      })
    } else {
      action.priority += v
      if (action.priority <= 0) {
        desiredActions = desiredActions.filter(v=>v!==action)
      }
    }
  }
  let keyup = (e: KeyboardEvent) => {
    if (e.key === 'ArrowLeft') {
      adjustAction('west', -1)
    } else if (e.key === 'ArrowRight') {
      adjustAction('east', -1)
    } else if (e.key === 'ArrowUp') {
      adjustAction('north', -1)
    } else if (e.key === 'ArrowDown') {
      adjustAction('south', -1)
    }
  }
  let keydown = (e: KeyboardEvent) => {
    if (e.repeat) return
    if (e.key === 'ArrowLeft') {
      adjustAction('west', 1)
    } else if (e.key === 'ArrowRight') {
      adjustAction('east', 1)
    } else if (e.key === 'ArrowUp') {
      adjustAction('north', 1)
    } else if (e.key === 'ArrowDown') {
      adjustAction('south', 1)
    }
  }
  let hookKeyboard = () => {
    window.addEventListener('keyup', keyup)
    window.addEventListener('keydown', keydown)
  }
  let unhookKeyboard = () => {
    window.removeEventListener('keyup', keyup)
    window.removeEventListener('keydown', keydown)
  }

  return {
    enter,
    leave,
    update,
  }
}