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

import Foundation
import SpriteKit

// Represents the orientation of a shape in increments of 90 degrees
enum Orientation: Int, CustomStringConvertible, CaseIterable {
    case Zero = 0, Ninety, OneEighty, TwoSeventy
    
    var description: String {
        switch self {
        case .Zero:
            return "0"
        case .Ninety:
            return "90"
        case .OneEighty:
            return "180"
        case .TwoSeventy:
            return "270"
        }
    }
    
    static func random() -> Orientation {
        return Orientation.allCases.shuffled().first!
    }
    
    // Returns the orientation resulting from a rotation in either direction.
    static func rotate(orientation: Orientation, clockwise: Bool) -> Orientation {
        var rotated = orientation.rawValue + (clockwise ? 1 : -1)
        if rotated > Orientation.TwoSeventy.rawValue {
            rotated = Orientation.Zero.rawValue
        } else if rotated < 0 {
            rotated = Orientation.TwoSeventy.rawValue
        }
        return Orientation(rawValue:rotated)!
    }
}

// The number of total shape varieties
let NumShapeTypes = 7

// Shape indexes
let FirstBlockIdx: Int = 0
let SecondBlockIdx: Int = 1
let ThirdBlockIdx: Int = 2
let FourthBlockIdx: Int = 3

// Base class representing a shape consisting of four blocks. Derived classes represent the different
// tetramino shapes.
class Shape: Hashable, CustomStringConvertible {
    // The blocks comprising the shape
    var blocks = Array<Block>()
    // The current orientation of the shape
    var orientation: Orientation
    // The column and row representing the shape's anchor point
    var column, row: Int
    
    // Required overrides
    
    // Subclasses must override this property
    // The color of the shape
    var color: BlockColor? {
        return nil
    }
    
    // Subclasses must override this property
    // Returns the relative positions of the shape blocks based on the shape orientation
    var blockRowColumnPositions: [Orientation: Array<(columnDiff: Int, rowDiff: Int)>] {
        return [:]
    }
    
    // Subclasses must override this property
    // Returns the blocks in a shape that are "on the bottom" based on the shape orientation
    var bottomBlocksForOrientations: [Orientation: Array<Block>] {
        return [:]
    }
    
    // Returns the blocks in this shape that are currently "on the bottom"
    var bottomBlocks: Array<Block> {
        guard let bottomBlocks = bottomBlocksForOrientations[orientation] else {
            return []
        }
        return bottomBlocks
    }
    
    // Equatable
    static func == (lhs: Shape, rhs: Shape) -> Bool {
        return lhs.row == rhs.row && lhs.column == rhs.column
    }
    
    // Hashable
    func hash(into hasher: inout Hasher) {
        blocks.forEach {b in
            hasher.combine(b.hashValue)
        }
    }
    
    // CustomStringConvertible
    var description: String {
        guard let color = color else {
            return "<invalid color> block facing \(orientation): \(blocks[FirstBlockIdx]), \(blocks[SecondBlockIdx]), \(blocks[ThirdBlockIdx]), \(blocks[FourthBlockIdx])"
        }
        return "\(color) block facing \(orientation): \(blocks[FirstBlockIdx]), \(blocks[SecondBlockIdx]), \(blocks[ThirdBlockIdx]), \(blocks[FourthBlockIdx])"
    }
    
    init(column: Int, row: Int, orientation: Orientation) {
        self.column = column
        self.row = row
        self.orientation = orientation
        initializeBlocks()
    }
    
    convenience init(column: Int, row: Int) {
        self.init(column: column, row: row, orientation: Orientation.random())
    }
    
    // Initialize the blocks of the shape based on the current position, color, and orientation.
    final func initializeBlocks() {
        guard let blockRowColumnTranslations = blockRowColumnPositions[orientation] else {
            return
        }
        
        blocks = blockRowColumnTranslations.map { (diff) -> Block in
            return Block(column: column + diff.columnDiff, row: row + diff.rowDiff, color: color!)
        }
    }
    
    // Rotate the blocks in this shape to a new orientation.
    final func rotateBlocks(orientation: Orientation) {
        guard let blockRowColumnTranslation: Array<(columnDiff: Int, rowDiff: Int)> = blockRowColumnPositions[orientation] else {
            return
        }
        for (idx, diff) in blockRowColumnTranslation.enumerated() {
            blocks[idx].column = column + diff.columnDiff
            blocks[idx].row = row + diff.rowDiff
        }
    }
    
    // Rotate the blocks in this shape clockwise.
    final func rotateClockwise() {
        let newOrientation = Orientation.rotate(orientation: orientation, clockwise: true)
        rotateBlocks(orientation: newOrientation)
        orientation = newOrientation
    }
    
    // Rotate the blocks in this shape counterclockwise.
    final func rotateCounterClockwise() {
        let newOrientation = Orientation.rotate(orientation: orientation, clockwise: false)
        rotateBlocks(orientation: newOrientation)
        orientation = newOrientation
    }
    
    // Updates block positions in this shape to one row down.
    final func lowerShapeByOneRow() {
        shiftBy(columns: 0, rows: 1)
    }
    
    // Updates block positions in this shape to one row up.
    final func raiseShapeByOneRow() {
        shiftBy(columns: 0, rows: -1)
    }
    
    // Updates block positions in this shape to one colukmn to the right.
    final func shiftRightByOneColumn() {
        shiftBy(columns: 1, rows: 0)
    }
    
    // Updates block positions in this shape to one column to the left.
    final func shiftLeftByOneColumn() {
        shiftBy(columns: -1, rows: 0)
    }
    
    // Shifts plock positions in this shape in a relative fashion.
    final func shiftBy(columns: Int, rows: Int) {
        self.column += columns
        self.row += rows
        for block in blocks {
            block.column += columns
            block.row += rows
        }
    }
    
    // Moves the shape to an absolute position.
    final func moveTo(column: Int, row: Int) {
        self.column = column
        self.row = row
        rotateBlocks(orientation: orientation)
    }
    
    // Generates a random shape.
    final class func random(startingColumn: Int, startingRow: Int) -> Shape {
        switch Int.random(in: 0 ..< NumShapeTypes) {
        case 0:
            return SquareShape(column: startingColumn, row: startingRow)
        case 1:
            return LineShape(column: startingColumn, row: startingRow)
        case 2:
            return TShape(column: startingColumn, row: startingRow)
        case 3:
            return LShape(column: startingColumn, row: startingRow)
        case 4:
            return JShape(column: startingColumn, row: startingRow)
        case 5:
            return SShape(column: startingColumn, row: startingRow)
        default:
            return ZShape(column: startingColumn, row: startingRow)
        }
    }
}
