import { ContextI } from "../ContextI"
import { segments } from "../shared/segments"
import { audio, playSong, disableSound, enableSound, stopSong, playSound } from '../shared/audio'
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"
import { Zone } from "../live/Zone"
import { Entity, Sensor } from "../live/Entity"
import { AnimalEntity, isAnimalEntity } from "../live/AnimalEntity"
import { Action, adjustAction } from "../live/Action"
import { WorldContext } from "../live/World"
import { animals } from "../data/animals"
import { GibletEntity } from "../live/GibletEntity"
import { bodyTextStyle, buttonBlurStyle, buttonHoverStyle } from "../styles"

export interface PIXIMissingColorMatrix extends PIXI.Filter {
  night(intensity: number, multiply: boolean): void
  contrast(amount: number, multiply: boolean): void
  brightness(amount: number, multiply: boolean): void
  saturate(amount: number, multiply: boolean): void
  hue(rotation: number, multiply: boolean): void
  colorTone(desaturation: number, toned: number, lightColor: number, darkColor: number, multiple: boolean): void
  reset(): void
}

export interface Layer {
  title: string
  container: PIXI.Container
  decorations: DecorationInstance[]
  colorMatrix: PIXIMissingColorMatrix
}

export function GameState(ctx: ContextI, selectedAnimal: string, selectedSegment: string): StateI {
  //disableSound()
  let isNight = false
  let modeTimer = 0
  let nightTime = 30 * 1000
  let dayTime = 30 * 1000
  let daysSurvived = -1
  let lastTime: number = performance.now()
  let player: AnimalEntity
  let nightIndicated = false

  // Game Over screenie
  let gameOverShown = false
  let gameOverPending = false
  let gameOverScreen = new PIXI.Container()
  let gameOverBackground = PIXI.Sprite.from(PIXI.Texture.WHITE)
  gameOverBackground.tint = 0x111111
  gameOverBackground.alpha = 0.75

  let gameOverTitle = new PIXI.Text('You Have Been Hunted', bodyTextStyle())
  let gameOverKills = new PIXI.Text('Hunted: ', bodyTextStyle())
  let gameOverKiller = new PIXI.Text('Slain By: ', bodyTextStyle())
  let gameOverDays = new PIXI.Text('You survived', bodyTextStyle())
  let returnToMain = new PIXI.Text('Hunt Again', buttonBlurStyle())
  returnToMain.interactive = true
  returnToMain.on('pointerup', () => {
    ctx.pop()
  })
  returnToMain.on('pointerover', () => {
    returnToMain.style = buttonHoverStyle()
  })
  returnToMain.on('pointerout', () => {
    returnToMain.style = buttonBlurStyle()
  })

  gameOverScreen.addChild(gameOverBackground)
  gameOverScreen.addChild(gameOverTitle)
  gameOverScreen.addChild(gameOverKills)
  gameOverScreen.addChild(gameOverKiller)
  gameOverScreen.addChild(gameOverDays)
  gameOverScreen.addChild(returnToMain)

  let updateGameOver = () => {
    gameOverScreen.width = ctx.app.view.width  - ctx.app.view.width / 4
    gameOverScreen.height = ctx.app.view.height  - ctx.app.view.height / 4
    gameOverScreen.x = ctx.app.view.width / 2 - gameOverScreen.width / 2
    gameOverScreen.y = ctx.app.view.height / 2 - gameOverScreen.height / 2
    let xPos = 0
    let yPos = 32

    gameOverBackground.width = gameOverScreen.width
    gameOverBackground.height = gameOverScreen.height

    gameOverTitle.x = gameOverScreen.width / 2 - gameOverTitle.width / 2
    gameOverTitle.y = yPos
    yPos += gameOverTitle.height + 32

    gameOverKiller.text = 'Slain By: ' + player.killer?.def.name
    gameOverKiller.x = gameOverScreen.width / 2 - gameOverKiller.width / 2
    gameOverKiller.y = yPos
    yPos += gameOverKiller.height + 32

    gameOverKills.text = 'Hunted: ' + player.kills
    gameOverKills.x = gameOverScreen.width / 2 - gameOverKills.width / 2
    gameOverKills.y = yPos
    yPos += gameOverKills.height + 32

    gameOverDays.text = 'You survived ' + daysSurvived + ' days.'
    gameOverDays.x = gameOverScreen.width / 2 - gameOverDays.width /2
    gameOverDays.y = yPos
    yPos += gameOverDays.height + 32

    returnToMain.x = gameOverScreen.width / 2 - returnToMain.width / 2
    returnToMain.y = yPos
    yPos += returnToMain.height + 32
  }

  //
  let clockCoverSprite = new SpriteInstance('ui.clock.cover.default.0')
  let clockMoonSprite = new SpriteInstance('ui.clock.moon.default.0')
  let clockSunSprite = new SpriteInstance('ui.clock.sun.default.0')
  let clockContainer = new PIXI.Container()
  {
    clockMoonSprite.container.position.set(
      32,
      0,
    )
    clockMoonSprite.container.pivot.set(
      24,
      24,
    )
    clockSunSprite.container.position.set(
      32,
      0,
    )
    clockSunSprite.container.pivot.set(
      24,
      24,
    )

    clockContainer.scale.set(2, 2)
    clockContainer.addChild(clockMoonSprite.container)
    clockContainer.addChild(clockSunSprite.container)
    clockContainer.addChild(clockCoverSprite.container)
  }

  let world: planck.World = planck.World({
    gravity: planck.Vec2(0, 0),
  })
  let worldBody: planck.Body = world.createBody({
    type: 'static',
    linearDamping: 1,
  })
  worldBody.setPosition(planck.Vec2(0, 0))
  let worldFixture = worldBody.createFixture({
    shape: planck.Box(1, 1),
    friction: 10,
    density: 100,
  })

  world.on('begin-contact', (contact: planck.Contact) => {
    let a = contact.getFixtureA().getUserData()
    let b = contact.getFixtureB().getUserData()
    if (a instanceof Zone) {
      if (b instanceof Entity) {
        b.addZoneContact(a)
      }
    } else if (a instanceof Entity) {
      if (b instanceof Entity) {
        a.addEntityContact(b)
        b.addEntityContact(a)
      }
    }
    if (a instanceof Sensor && a.entity instanceof AnimalEntity) {
      if (b instanceof AnimalEntity) {
        a.entity.sense(a, b)
      }
    }
    if (b instanceof Sensor && b.entity instanceof AnimalEntity) {
      if (a instanceof AnimalEntity) {
        b.entity.sense(b, a)
      }
    }
  })
  world.on('end-contact', (contact: planck.Contact) => {
    let a = contact.getFixtureA().getUserData()
    let b = contact.getFixtureB().getUserData()
    if (a instanceof Zone) {
      if (b instanceof Entity) {
        b.removeZoneContact(a)
      }
    } else if (a instanceof Entity) {
      if (b instanceof Entity) {
        a.removeEntityContact(b)
        b.removeEntityContact(a)
      }
    }
    if (a instanceof Sensor && a.entity instanceof AnimalEntity) {
      if (b instanceof AnimalEntity) {
        a.entity.lost(a, b)
      }
    }
    if (b instanceof Sensor && b.entity instanceof AnimalEntity) {
      if (a instanceof AnimalEntity) {
        b.entity.lost(b, a)
      }
    }
  })

  let realRootContainer = new PIXI.Container()
  let rootContainer = new PIXI.Container()
  let playLayer: Layer
  let layers: Layer[] = []
  let entities: Entity[] = []
  let zones: Zone[] = []
  let spawnZones: Record<string, Zone[]> = {}

  let enter = () => {
    realRootContainer.addChild(rootContainer)
    lastTime = performance.now()
    hookKeyboard()
    // Load the world segment.
    let w = segments[selectedSegment]
    if (!w) return ctx.pop()

    // Add our world border.
    let borderShape = planck.Chain([planck.Vec2(0, 0), planck.Vec2(w.width, 0), planck.Vec2(w.width, w.height), planck.Vec2(0, w.height), planck.Vec2(0, 0)], true)
    let borderFixture = worldBody.createFixture({
      shape: borderShape,
    })

    rootContainer.width = w.width
    rootContainer.height = w.height
    rootContainer.scale.set(2, 2)
    for (let l of w.layers) {
      let layer: Layer = {
        title: l.title,
        container: new PIXI.Container(),
        decorations: [],
        colorMatrix: new PIXI.filters.ColorMatrixFilter()
      }
      layer.container.filters = [layer.colorMatrix]
      layer.container.width = w.width
      layer.container.height = w.height

      if (l.title === 'objects') {
        playLayer = layer
        layer.container.sortableChildren = true
        // Quick, make the gibs layer!
        let gibLayer: Layer = {
          title: 'gibs',
          container: new PIXI.Container(),
          decorations: [],
          colorMatrix: new PIXI.filters.ColorMatrixFilter()
        }
        gibLayer.container.filters = [gibLayer.colorMatrix]
        gibLayer.container.width = w.width
        gibLayer.container.height = w.height
        rootContainer.addChild(gibLayer.container)
        layers.push(gibLayer)
      }
      layers.push(layer)

      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--
          }
        }
        if (l.title === 'objects') {
          di.container.zIndex = di.container.position.y + di.container.height/4
        }
        layer.container.addChild(di.container)
        layer.decorations.push(di)
      }

      rootContainer.addChild(layer.container)
    }

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

    let playerSpawn = getSpawnZone('spawn')
    if (!playerSpawn) {
      // return to menu with an error?
    } else {
      let bounds = playerSpawn.bounds
      let x = bounds[0] + Math.floor(Math.random() * bounds[2])
      let y = bounds[1] + Math.floor(Math.random() * bounds[3])
      // Add player entity
      player = new AnimalEntity(animals[selectedAnimal])
      player.isPlayer = true
      addEntity(player, 'objects', x, y)
      player.turn()
    }

    progress()

    // Add clock

    ctx.app.stage.addChild(realRootContainer)
    ctx.app.stage.addChild(clockContainer)
  }
  let leave = () => {
    unhookKeyboard()
    for (let entity of entities) {
      removeEntity(entity)
    }
    for (let zone of zones) {
      removeZone(zone)
    }
    stopSong()
    ctx.app.stage.removeChild(realRootContainer)
    ctx.app.stage.removeChild(clockContainer)
  }
  let elapsed: number = 0
  let update = (delta: number) => {
    let time = performance.now()
    let realDelta = time - lastTime
    lastTime = time
    checkGamepads()
    elapsed += delta
    modeTimer += realDelta

    if (player.dead) {
      if (!gameOverPending && !gameOverShown) {
        gameOverPending = true
        setTimeout(() => {
          gameOverPending = false
          gameOverShown = true
          realRootContainer.addChild(gameOverScreen)
          updateGameOver()
        }, 3000)
      } else if (gameOverShown) {
        updateGameOver()
      }
    } else {
      // If it's just the player at night, accelerate time.
      if (entities.filter(v=>v instanceof AnimalEntity).length === 1) {
        if (isNight) {
          modeTimer += realDelta * 4
        } else {
          modeTimer += realDelta
        }
      }
      // Update clock
      {
        //clockContainer.width = ctx.app.renderer.width
        let w = ctx.app.renderer.width
        let h = ctx.app.renderer.height
        clockContainer.x = w / 2 - clockCoverSprite.container.width
        clockContainer.y = h - clockCoverSprite.container.height * 2 - 8
        if (isNight) {
          clockMoonSprite.container.angle = 180 + modeTimer / nightTime * 180
          clockSunSprite.container.angle = modeTimer / nightTime * 180
        } else {
          clockMoonSprite.container.angle = modeTimer / dayTime * 180
          clockSunSprite.container.angle = 180 + modeTimer / dayTime * 180
        }
        clockMoonSprite.container.angle -= 230
        clockSunSprite.container.angle -= 230
      }

      // If we're getting within 3 seconds of night, play audio.
      if (!isNight && !nightIndicated && modeTimer >= nightTime - 5000) {
        playSound('action/warning', 1)
        nightIndicated = true
      }

      if (isNight && modeTimer >= nightTime) {
        modeTimer = 0
        nightfall(false)
      } else if (!isNight && modeTimer >= dayTime) {
        modeTimer = 0
        nightfall(true)
      }
    }
    // Run world sim.
    while (elapsed >= 1/6) {
      world.step(1/6)
      elapsed -= 1/6
    }
    // Update/render.
    /*for (let layer of layers) {
      for (let decoration of layer.decorations) {
        decoration.update(realDelta)
      }
    }*/
    for (let entity of entities) {
      if (isAnimalEntity(entity)) {
        if (entity.isPlayer) {
          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(entity.x, entity.y)

          //rootContainer.pivot.set(Math.max(w.width/6, Math.min(entity.x, w.width/3)), Math.max(w.height/6, Math.min(entity.y, w.height/3)))
        } else {
          entity.think(realDelta)
        }
      } else {
        //console.log('hmm, we do be tickin', entity, entity.x, entity.y)
      }
      entity.update(realDelta, worldContext)
      // I guess...
      entity.sprite.container.x = entity.x - entity.sprite.container.pivot.x
      entity.sprite.container.y = entity.y
      entity.sprite.container.zIndex = entity.sprite.container.y - entity.sprite.container.pivot.y
      //entity.y + entity.sprite.originY
    }
    // I guess this is okay.
    for (let entity of entities) {
      if (entity.shouldRemove) {
        if (entity instanceof AnimalEntity) {
          removeEntity(entity)
        }
      }
    }
    //entities = entities.filter(v=>v.shouldRemove)
  }

  let addEntity = (entity: Entity, layerTitle: string, x: number, y: number) => {
    if (entities.find(v=>v===entity)) return
    entities.push(entity)
    let layer = layers.find(v=>v.title===layerTitle)
    if (!layer) layer = playLayer
    layer.container.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) {
        entity.shape = shape
        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,
        })
        fixture.setUserData(entity)
        entity.body = body
        body.setUserData(entity)
        // Sensors
        let defaultShort = 50
        let defaultLong = 100
        if (entity instanceof AnimalEntity) {
          defaultLong = entity.def.animal.scent
          defaultShort = entity.def.animal.sight
        }
        if (entity instanceof AnimalEntity && !entity.isPlayer) {
          // Create a short sensor (vision).
          let senseFixture = body.createFixture({
            shape: planck.Circle(planck.Vec2(entity.x, entity.y), defaultShort),
            isSensor: true,
            filterGroupIndex: -8
          })
          senseFixture.setUserData(new Sensor(entity, senseFixture, 'short'))
          // Create a long sensor (scent).
          let longSenseFixture = body.createFixture({
            shape: planck.Circle(planck.Vec2(entity.x, entity.y), defaultLong),
            isSensor: true,
            filterGroupIndex: -8
          })
          longSenseFixture.setUserData(new Sensor(entity, longSenseFixture, 'long'))
        }
      }
    }
    entity.x = x
    entity.y = y
  }
  let removeEntity = (entity: Entity) => {
    entities = entities.filter(v=>v!==entity)
    entity.sprite.container.parent.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
    // We're using triggers for spawning due to laziness.
    if (zone.type === 'trigger') {
      if (!spawnZones[zone.event]) {
        spawnZones[zone.event] = []
      }
      spawnZones[zone.event].push(zone)
    } else {
      let shape = planck.Polygon(zone.points.map(v=>planck.Vec2(v[0],v[1])))
      zone.fixture = worldBody.createFixture({
        shape: shape,
      })
      if (zone.type === 'fluid') {
        zone.fixture.setSensor(true)
      }
      zone.fixture.setUserData(zone)
    }
    zones.push(zone)
  }
  let removeZone = (zone: Zone) => {
    zones = zones.filter(v=>v!==zone)
    if (zone.fixture) {
      worldBody.destroyFixture(zone.fixture)
    }
  }
  let getSpawnZone = (name: string): Zone|undefined => {
    if (!spawnZones[name] || !spawnZones[name].length) {
      return undefined
    }
    return spawnZones[name][Math.floor(Math.random()*spawnZones[name].length)]
  }

  let worldContext: WorldContext = {
    addEntity,
    removeEntity,
    queryAABB: (aabb: planck.AABB, callback: (fixture: planck.Fixture) => boolean) => {
      world.queryAABB(aabb, callback)
    },
    rayCast: (point1: planck.Vec2, point2: planck.Vec2, callback: (fixture: planck.Fixture, point: planck.Vec2, normal: planck.Vec2, fraction: number) => boolean) => {
      world.rayCast(point1, point2, callback)
    },
    queryEntities: (min: [number, number], max: [number, number]): Entity[] => {
      let ret: Entity[] = []
      for (let entity of entities) {
        if (!entity.shape) {
          continue
        }
        let p = entity.position
        let r = entity.shape.getRadius()
        /*let a = {
          x: min[0],
          w: max[0] - min[0],
          y: min[1],
          h: max[1] - min[1],
        }
        let b = {
          x: p[0],
          w: r,
          y: p[1],
          h: r,
        }*/
        if (
          (Math.abs(min[0] - (p[0]-r)) * 2 < ((max[0]-min[0]) + r)) &&
          (Math.abs(min[1] - (p[1]-r)) * 2 < ((max[1]-min[1]) + r))) {
            ret.push(entity)
        }
      }
      return ret
    },
  }

  let desiredActions: Action[] = []
  let keyup = (e: KeyboardEvent) => {
    if (e.key === 'ArrowLeft' || e.key === 'h') {
      desiredActions = adjustAction(desiredActions, 'west', 0)
    } else if (e.key === 'ArrowRight' || e.key === 'l') {
      desiredActions = adjustAction(desiredActions, 'east', 0)
    } else if (e.key === 'ArrowUp' || e.key === 'k') {
      desiredActions = adjustAction(desiredActions, 'north', 0)
    } else if (e.key === 'ArrowDown' || e.key === 'j') {
      desiredActions = adjustAction(desiredActions, 'south', 0)
    } else if (e.key === 'z' || e.key === 'Control') {
      desiredActions = adjustAction(desiredActions, 'attack', 0)
    } else if (e.key === 'Escape') {
      ctx.pop()
    }
  }
  let keydown = (e: KeyboardEvent) => {
    if (e.repeat) return
    if (e.key === 'ArrowLeft' || e.key === 'h') {
      desiredActions = adjustAction(desiredActions, 'west', 1)
    } else if (e.key === 'ArrowRight' || e.key === 'l') {
      desiredActions = adjustAction(desiredActions, 'east', 1)
    } else if (e.key === 'ArrowUp' || e.key === 'k') {
      desiredActions = adjustAction(desiredActions, 'north', 1)
    } else if (e.key === 'ArrowDown' || e.key === 'j') {
      desiredActions = adjustAction(desiredActions, 'south', 1)
    } else if (e.key === 'z' || e.key === 'Control') {
      desiredActions = adjustAction(desiredActions, 'attack', 2)
    }
  }
  let hookKeyboard = () => {
    window.addEventListener('keyup', keyup)
    window.addEventListener('keydown', keydown)
  }
  let unhookKeyboard = () => {
    window.removeEventListener('keyup', keyup)
    window.removeEventListener('keydown', keydown)
  }

  let gamepads: Record<string, Gamepad> = {}
  window.addEventListener('gamepadconnected', (ev: GamepadEvent) => {
    gamepads[ev.gamepad.id] = ev.gamepad
  })
  window.removeEventListener('gamepaddisconnected', (ev: GamepadEvent) => {
    delete gamepads[ev.gamepad.id]
  })
  function checkGamepads() {
    //for (let gp of Object.values(gamepads)) {
    for (let gp of navigator.getGamepads()) {
      if (!gp || !gp.connected) break
      if (gp.axes[0] < 0) {
        desiredActions = adjustAction(desiredActions, 'west', Math.abs(gp.axes[0]))
      } else if (gp.axes[0] > 0) {
        desiredActions = adjustAction(desiredActions, 'east', gp.axes[0])
      } else {
        desiredActions = adjustAction(desiredActions, 'east', 0)
        desiredActions = adjustAction(desiredActions, 'west', 0)
      }
      if (gp.axes[1] < 0) {
        desiredActions = adjustAction(desiredActions, 'north', Math.abs(gp.axes[1]))
      } else if (gp.axes[1] > 0) {
        desiredActions = adjustAction(desiredActions, 'south', gp.axes[1])
      } else {
        desiredActions = adjustAction(desiredActions, 'north', 0)
        desiredActions = adjustAction(desiredActions, 'south', 0)
      }
      let attackHeld = false
      for (let i = 0; i < 4; i++) {
        let btn = gp.buttons[i]
        if (btn.pressed) {
          attackHeld = true
        }
      }
      if (attackHeld) {
        desiredActions = adjustAction(desiredActions, 'attack', 2)
      } else if (desiredActions.find(v=>v.type==='attack')) {
        desiredActions = adjustAction(desiredActions, 'attack', 0)
      }
    }
  }

  let nightfall = (b: boolean) => {
    if (b) {
      playSong('GGJ-ScaryMusic')
      for (let l of layers) {
        //l.colorMatrix.brightness(0.25, false)
        l.colorMatrix.night(0.2, true)
        l.colorMatrix.saturate(-0.75, true)
        //l.colorMatrix.hue(160, true)
      }
      for (let e of entities) {
        if (e instanceof AnimalEntity) {
          e.turn()
        }
      }
    } else {
      nightIndicated = false
      playSong('GGJ-HappyMusic')
      for (let l of layers) {
        l.colorMatrix.reset()
      }
      for (let e of entities) {
        if (e instanceof AnimalEntity) {
          e.turn()
        }
      }
      progress()
    }

    isNight = b
  }

  let progress = () => {
    daysSurvived++

    for (let animal of ['turkey','nutria','deer','salamander','rabbit']) {
      let count = 1 + Math.floor(Math.random() * daysSurvived)
      for (let i = 0; i < count; i++) {
        let spawn = getSpawnZone(animal)
        if (!spawn) break
        let bounds = spawn.bounds
        let x = bounds[0] + Math.floor(Math.random() * bounds[2])
        let y = bounds[1] + Math.floor(Math.random() * bounds[3])

        let entity = new AnimalEntity(animals[animal])

        addEntity(entity, 'objects', x, y)
      }
    }

    nightTime = (30+daysSurvived) * 1000
    dayTime = (30-daysSurvived) * 1000
  }

  nightfall(false)

  return {
    enter,
    leave,
    update,
  }
}