import { always, equals, findIndex, head, map, pipe, range, reject, remove, toString, view } from 'ramda'
import { P, match } from 'ts-pattern'
import { costMoney } from '../board/resource'
import { activeLens, subtractCoins, withActivePlayer } from '../board/player'
import { GameState, GameCommandBuyDistrictParams, Tile, LandEnum, BuildingEnum, GameCommandEnum } from '../types'

const checkCanGetLandscape = (state?: GameState): GameState | undefined => {
  if (state === undefined) return undefined
  if (state.frame!.currentPlayerIndex !== state.frame!.activePlayerIndex) return undefined
  if (state.frame!.canBuyLandscape) return state
  if (state.frame!.bonusActions.includes(GameCommandEnum.BUY_DISTRICT)) return state
  return undefined
}

const checkForConnection = (y: number) =>
  // new district must have an existing
  withActivePlayer((player) => {
    const { landscape, landscapeOffset } = player
    const north = landscape[y + landscapeOffset - 1]?.[4]?.[0]
    const east = landscape[y + landscapeOffset]?.[5]?.[0]
    const west = landscape[y + landscapeOffset]?.[1]?.[0]
    const south = landscape[y + landscapeOffset + 1]?.[4]?.[0]
    if (north || east || west || south) return player
    return undefined
  })

const checkForOverlap = (y: number) =>
  withActivePlayer((player) => {
    const { landscape, landscapeOffset } = player
    if (landscape[y + landscapeOffset]?.[3]?.[0] !== undefined) return undefined
    return player
  })

const checkCanAfford = (state?: GameState): GameState | undefined => {
  if (state?.frame!.bonusActions?.includes(GameCommandEnum.BUY_DISTRICT)) return state
  const cost = state?.districtPurchasePrices![0]
  return withActivePlayer((player) => {
    if (cost === undefined) return undefined
    if (costMoney(player) < cost) return undefined
    return player
  })(state)
}

const payForDistrict = (state?: GameState): GameState | undefined => {
  if (state === undefined) return undefined
  if (state.frame!.bonusActions?.includes(GameCommandEnum.BUY_DISTRICT)) return state
  const cost = state.districtPurchasePrices![0]
  return withActivePlayer(subtractCoins(cost))(state)
}

const removeDistrictFromPool = (state?: GameState): GameState | undefined => {
  if (state === undefined) return undefined
  if (state.frame!.bonusActions?.includes(GameCommandEnum.BUY_DISTRICT)) return state
  return {
    ...state,
    districtPurchasePrices: state.districtPurchasePrices!.slice(1),
  }
}

const denyBuyingAnyMoreLandscape = (state?: GameState): GameState | undefined => {
  if (state === undefined) return undefined
  const atIndex = findIndex((a: GameCommandEnum) => equals(a, GameCommandEnum.BUY_DISTRICT), state.frame!.bonusActions)
  return {
    ...state,
    frame: {
      ...state.frame!,
      // if the command was found in bonusActions
      ...(atIndex !== -1
        ? {
            // leave canBuyLandscape as it was
            bonusActions: remove(atIndex, 1, state.frame!.bonusActions),
          }
        : {
            // otherwise set canBuyLandscape to false
            canBuyLandscape: false,
          }),
    },
  }
}

const NEW_ROW: Tile[] = [[], [], [], [], [], [], [], [], []]
const expandLandscape = (y: number) =>
  withActivePlayer((player) => {
    const landscape = [...player.landscape]
    let { landscapeOffset } = player
    if (y + player.landscapeOffset < 0) {
      landscape.unshift(NEW_ROW)
      landscapeOffset++
    } else if (y + player.landscapeOffset >= landscape.length) {
      landscape.push(NEW_ROW)
    }
    return {
      ...player,
      landscape,
      landscapeOffset,
    }
  })

const newDistrict = (side: 'PLAINS' | 'HILLS'): Tile[] =>
  match(side)
    .with('HILLS', () => [
      [LandEnum.Plains, BuildingEnum.Moor] as Tile,
      [LandEnum.Plains, BuildingEnum.Forest] as Tile,
      [LandEnum.Plains, BuildingEnum.Forest] as Tile,
      [LandEnum.Hillside] as Tile,
      [LandEnum.Hillside] as Tile,
    ])
    .with('PLAINS', () => [
      [LandEnum.Plains, BuildingEnum.Forest] as Tile,
      [LandEnum.Plains] as Tile,
      [LandEnum.Plains] as Tile,
      [LandEnum.Plains] as Tile,
      [LandEnum.Hillside] as Tile,
    ])
    .exhaustive()

const addNewDistrict = (y: number, side: 'PLAINS' | 'HILLS') =>
  withActivePlayer((player) => {
    const newTiles = newDistrict(side)
    const rowsBefore = player.landscape.slice(0, y + player.landscapeOffset)
    const newRow = [
      ...player.landscape[y + player.landscapeOffset].slice(0, 2),
      ...newTiles,
      ...player.landscape[y + player.landscapeOffset].slice(7, 9),
    ]
    const rowsAfter = player.landscape.slice(y + player.landscapeOffset + 1)
    const landscape = [...rowsBefore, newRow, ...rowsAfter]
    return {
      ...player,
      landscape,
    }
  })

export const buyDistrict = ({ side, y }: GameCommandBuyDistrictParams) =>
  pipe(
    //
    checkCanGetLandscape,
    checkForOverlap(y),
    checkForConnection(y),
    checkCanAfford,
    payForDistrict,
    removeDistrictFromPool,
    denyBuyingAnyMoreLandscape,
    expandLandscape(y),
    addNewDistrict(y, side)
  )

export const complete =
  (state: GameState) =>
  (partial: string[]): string[] => {
    const player = view(activeLens(state), state)
    return match<string[], string[]>(partial)
      .with([], () => {
        if (state.frame!.bonusActions.includes(GameCommandEnum.BUY_DISTRICT) && head(state.districtPurchasePrices!))
          return [GameCommandEnum.BUY_DISTRICT]
        if (checkCanGetLandscape(state) === undefined) return []
        const playerWealth = costMoney(player)
        const nextDistrictCost = head(state.districtPurchasePrices!) ?? Infinity
        if (playerWealth < nextDistrictCost) return []
        return [GameCommandEnum.BUY_DISTRICT]
      })
      .with([GameCommandEnum.BUY_DISTRICT], () =>
        map(
          toString,
          reject(
            (y: number) => !checkForConnection(y)(state) || !checkForOverlap(y)(state),
            range(-1 - player.landscapeOffset, 1 + player.landscape.length - player.landscapeOffset)
          )
        )
      )
      .with([GameCommandEnum.BUY_DISTRICT, P._], always(['PLAINS', 'HILLS']))
      .with([GameCommandEnum.BUY_DISTRICT, P._, P._], always(['']))
      .otherwise(always([]))
  }
