//
//  Privyet.swift
//  Privyet
//
//  Created by Amy Bowersox on 5/24/20.
//  Copyright © 2020 Erbosoft Metaverse Design Solutions. All rights reserved.
//

import Foundation

let NumColumns = 10
let NumRows = 20

let StartingColumn = 4
let StartingRow = 0

let PreviewColumn = 12
let PreviewRow = 3

let PointsPerLine = 10
let LevelThreshold = 500

protocol PrivyetDelegate {
    // Invoked when the current round of Privyet ends
    func gameDidEnd(privyet: Privyet)
    
    // Invoked after a game has begun
    func gameDidBegin(privyet: Privyet)
    
    // Invoked when the falling shape has become part of the game board
    func gameShapeDidLand(privyet: Privyet)
    
    // Invoked when the falling shape has changed its location
    func gameShapeDidMove(privyet: Privyet)
    
    // invoked when the falling shape has changed its location after being dropped
    func gameShapeDidDrop(privyet: Privyet)
    
    // invoked when the game has reached a new level
    func gameDidLevelUp(privyet: Privyet)
}

class Privyet {
    var bucket: Array2D<Block>
    var nextShape: Shape?
    var fallingShape: Shape?
    var delegate: PrivyetDelegate?
    
    var score = 0
    var level = 1
    
    init() {
        fallingShape = nil
        nextShape = nil
        bucket = Array2D<Block>(columns: NumColumns, rows: NumRows)
    }
    
    // Starts the game by initializing game data
    func beginGame() {
        if (nextShape == nil) {
            nextShape = Shape.random(startingColumn: PreviewColumn, startingRow: PreviewRow)
        }
        delegate?.gameDidBegin(privyet: self)
    }
    
    // Advances the current falling shape and the "next" shape. Returns both these shapes as a tuple.
    func newShape() -> (fallingShape: Shape?, nextShape: Shape?) {
        fallingShape = nextShape
        nextShape = Shape.random(startingColumn: PreviewColumn, startingRow: PreviewRow)
        fallingShape?.moveTo(column: StartingColumn, row: StartingRow)
        
        guard detectIllegalPlacement() == false else {
            nextShape = fallingShape
            nextShape!.moveTo(column: PreviewColumn, row: PreviewRow)
            endGame()
            return (nil, nil)
        }
        
        return (fallingShape, nextShape)
    }
    
    // Returns true if the falling shape is in an "illegal" placement (outside bucket bounds or "colliding"
    // with existing blocks in the bucket).
    func detectIllegalPlacement() -> Bool {
        guard let shape = fallingShape else {
            return false
        }
        for block in shape.blocks {
            if block.column < 0 || block.column >= NumColumns || block.row < 0 || block.row >= NumRows {
                return true
            } else if bucket[block.column, block.row] != nil {
                return true
            }
        }
        return false
    }
    
    // "Settle" the falling shape by transferring its blocks to the bucket.
    func settleShape() {
        guard let shape = fallingShape else {
            return
        }
        for block in shape.blocks {
            bucket[block.column, block.row] = block
        }
        fallingShape = nil
        delegate?.gameShapeDidLand(privyet: self)
    }

    // Returns true if the falling block is "landing" (at the bottom of the bucket or directly above existing blocks)
    func detectTouch() -> Bool {
        guard let shape = fallingShape else {
            return false
        }
        for bottomBlock in shape.bottomBlocks {
            if bottomBlock.row == NumRows - 1 || bucket[bottomBlock.column, bottomBlock.row + 1] != nil {
                return true
            }
        }
        return false
    }
    
    // Ends the current game
    func endGame() {
        score = 0
        level = 1
        delegate?.gameDidEnd(privyet: self)
    }
    
    // Detect and remove completed lines. Returns a tuple of two arrays of arrays:
    // linesRemoved - The blocks that will be removed as a result of completed lines.
    // fallenBlocks - The blocks that will drop to a lower line in the bucket when the above blocks are removed.
    // If no lines are to be removed, returns a pair of empty lists.
    func removeCompletedLines() -> (linesRemoved: Array<Array<Block>>, fallenBlocks: Array<Array<Block>>) {
        var removedLines = Array<Array<Block>>()
        for row in (1..<NumRows).reversed() {
            var rowOfBlocks = Array<Block>()
            for column in 0..<NumColumns {
                guard let block = bucket[column, row] else {
                    continue
                }
                rowOfBlocks.append(block)
            }
            if rowOfBlocks.count == NumColumns {
                // filled line detected, remove it
                removedLines.append(rowOfBlocks)
                for block in rowOfBlocks {
                    bucket[block.column, block.row] = nil
                }
            }
        }
        if removedLines.count == 0 {
            return ([], []) // no lines filled
        }
        
        // Advance score and game level as appropriate
        let pointsEarned = removedLines.count * PointsPerLine * level
        score += pointsEarned
        if score >= level * LevelThreshold {
            level += 1
            delegate?.gameDidLevelUp(privyet: self)
        }
        
        var fallenBlocks = Array<Array<Block>>()
        for column in 0..<NumColumns {
            var fallenBlocksArray = Array<Block>()
            for row in (1..<removedLines[0][0].row).reversed() {
                guard let block = bucket[column, row] else {
                    continue
                }
                var newRow = row
                while (newRow < NumRows - 1 && bucket[column, newRow + 1] == nil) {
                    newRow += 1
                }
                block.row = newRow
                bucket[column, row] = nil
                bucket[column, newRow] = block
                fallenBlocksArray.append(block)
            }
            if fallenBlocksArray.count > 0 {
                fallenBlocks.append(fallenBlocksArray)
            }
        }
        return (removedLines, fallenBlocks)
    }
    
    // Removes all blocks from the bucket (at end of game). Returns all the removed blocks.
    func removeAllBlocks() -> Array<Array<Block>> {
        var allBlocks = Array<Array<Block>>()
        for row in 0..<NumRows {
            var rowOfBlocks = Array<Block>()
            for column in 0..<NumColumns {
                guard let block = bucket[column, row] else {
                    continue
                }
                rowOfBlocks.append(block)
                bucket[column, row] = nil
            }
            allBlocks.append(rowOfBlocks)
        }
        return allBlocks
    }
    
    // Drop the falling shape down as far as it will go.
    func dropShape() {
        guard let shape = fallingShape else {
            return
        }
        while detectIllegalPlacement() == false {
            shape.lowerShapeByOneRow()
        }
        shape.raiseShapeByOneRow()
        delegate?.gameShapeDidDrop(privyet: self)
    }
    
    // Let the falling shape fall by one row.
    func letShapeFall() {
        guard let shape = fallingShape else {
            return
        }
        shape.lowerShapeByOneRow()
        if detectIllegalPlacement() {
            shape.raiseShapeByOneRow()
            if detectIllegalPlacement() {
                endGame()  // shape was in illegal place to start, this ends the game
            } else {
                settleShape() // shape can land
            }
        } else {
            delegate?.gameShapeDidMove(privyet: self)
            if detectTouch() {
                settleShape()
            }
        }
    }
    
    // Rotate the falling shape clockwise by 90 degrees.
    func rotateShape() {
        guard let shape = fallingShape else {
            return
        }
        shape.rotateClockwise()
        guard detectIllegalPlacement() == false else {
            shape.rotateCounterClockwise()
            return
        }
        delegate?.gameShapeDidMove(privyet: self)
    }
    
    // Move the falling shape left by one column.
    func moveShapeLeft() {
        guard let shape = fallingShape else {
            return
        }
        shape.shiftLeftByOneColumn()
        guard detectIllegalPlacement() == false else {
            shape.shiftRightByOneColumn()
            return
        }
        delegate?.gameShapeDidMove(privyet: self)
    }
    
    // Move the falling shape right by one column.
    func moveShapeRight() {
        guard let shape = fallingShape else {
            return
        }
        shape.shiftRightByOneColumn()
        guard detectIllegalPlacement() == false else {
            shape.shiftLeftByOneColumn()
            return
        }
        delegate?.gameShapeDidMove(privyet: self)
    }
}
