// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { ClonesWithImmutableArgs } from "@cwia/ClonesWithImmutableArgs.sol";

import { Claim } from "../libraries/DisputeTypes.sol";
import { Hash } from "../libraries/DisputeTypes.sol";
import { GameType } from "../libraries/DisputeTypes.sol";

import { NoImplementation } from "../libraries/DisputeErrors.sol";
import { GameAlreadyExists } from "../libraries/DisputeErrors.sol";

import { IDisputeGame } from "./IDisputeGame.sol";
import { IDisputeGameFactory } from "./IDisputeGameFactory.sol";

/**
 * @title DisputeGameFactory
 * @notice A factory contract for creating `IDisputeGame` contracts.
 */
contract DisputeGameFactory is Ownable, IDisputeGameFactory {
    /**
     * @dev Allows for the creation of clone proxies with immutable arguments.
     */
    using ClonesWithImmutableArgs for address;

    /**
     * @notice Mapping of `GameType`s to their respective `IDisputeGame` implementations.
     */
    mapping(GameType => IDisputeGame) public gameImpls;

    /**
     * @notice Mapping of a hash of `gameType . rootClaim . extraData` to
     *         the deployed `IDisputeGame` clone.
     * @dev Note: `.` denotes concatenation.
     */
    mapping(Hash => IDisputeGame) internal disputeGames;

    /**
     * @notice Constructs a new DisputeGameFactory contract.
     * @param _owner The owner of the contract.
     */
    constructor(address _owner) Ownable() {
        transferOwnership(_owner);
    }

    /**
     * @notice Retrieves the hash of `gameType . rootClaim . extraData`
     *         to the deployed `DisputeGame` clone.
     * @dev Note: `.` denotes concatenation.
     * @param gameType The type of the DisputeGame.
     *        Used to decide the implementation to clone.
     * @param rootClaim The root claim of the DisputeGame.
     * @param extraData Any extra data that should be provided to the
     *        created dispute game.
     * @return _proxy The clone of the `DisputeGame` created with the
     *         given parameters. `address(0)` if nonexistent.
     */
    function games(
        GameType gameType,
        Claim rootClaim,
        bytes calldata extraData
    ) external view returns (IDisputeGame _proxy) {
        return disputeGames[getGameUUID(gameType, rootClaim, extraData)];
    }

    /**
     * @notice Creates a new DisputeGame proxy contract.
     * @notice If a dispute game with the given parameters already exists,
     *         it will be returned.
     * @param gameType The type of the DisputeGame.
     *        Used to decide the proxy implementation.
     * @param rootClaim The root claim of the DisputeGame.
     * @param extraData Any extra data that should be provided
     *        to the created dispute game.
     * @return proxy The clone of the `DisputeGame`.
     */
    function create(
        GameType gameType,
        Claim rootClaim,
        bytes calldata extraData
    ) external returns (IDisputeGame proxy) {
        // Grab the implementation contract for the given `GameType`.
        IDisputeGame impl = gameImpls[gameType];

        // If there is no implementation to clone for the given `GameType`, revert.
        if (address(impl) == address(0)) {
            revert NoImplementation(gameType);
        }

        // Clone the implementation contract and initialize it with the given parameters.
        bytes memory data = abi.encodePacked(rootClaim, extraData);
        proxy = IDisputeGame(address(impl).clone(data));
        proxy.initialize();

        // Compute the unique identifier for the dispute game.
        Hash uuid = getGameUUID(gameType, rootClaim, extraData);

        // If a dispute game with the same UUID already exists, revert.
        if (address(disputeGames[uuid]) != address(0)) {
            revert GameAlreadyExists(uuid);
        }

        // Store the dispute game in the mapping & emit the `DisputeGameCreated` event.
        disputeGames[uuid] = proxy;
        emit DisputeGameCreated(address(proxy), gameType, rootClaim);
    }

    /**
     * @notice Sets the implementation contract for a specific `GameType`.
     * @param gameType The type of the DisputeGame.
     * @param impl The implementation contract for the given `GameType`.
     */
    function setImplementation(GameType gameType, IDisputeGame impl) external onlyOwner {
        gameImpls[gameType] = impl;
    }

    /**
     * @notice Returns a unique identifier for the given dispute game parameters.
     * @dev Hashes the concatenation of `gameType . rootClaim . extraData`
     *      without expanding memory.
     * @param gameType The type of the DisputeGame.
     * @param rootClaim The root claim of the DisputeGame.
     * @param extraData Any extra data that should be provided to the created dispute game.
     * @return _uuid The unique identifier for the given dispute game parameters.
     */
    function getGameUUID(
        GameType gameType,
        Claim rootClaim,
        bytes memory extraData
    ) public pure returns (Hash _uuid) {
        assembly {
            // Grab the offsets of the other memory locations we will need to temporarily overwrite.
            let gameTypeOffset := sub(extraData, 0x60)
            let rootClaimOffset := add(gameTypeOffset, 0x20)
            let pointerOffset := add(rootClaimOffset, 0x20)

            // Copy the memory that we will temporarily overwrite onto the stack
            // so we can restore it later
            let tempA := mload(gameTypeOffset)
            let tempB := mload(rootClaimOffset)
            let tempC := mload(pointerOffset)

            // Overwrite the memory with the data we want to hash
            mstore(gameTypeOffset, gameType)
            mstore(rootClaimOffset, rootClaim)
            mstore(pointerOffset, 0x60)

            // Compute the length of the memory to hash
            // `0x60 + 0x20 + extraData.length` rounded to the *next* multiple of 32.
            let hashLen := and(add(mload(extraData), 0x9F), not(0x1F))

            // Hash the memory to produce the UUID digest
            _uuid := keccak256(gameTypeOffset, hashLen)

            // Restore the memory prior to `extraData`
            mstore(gameTypeOffset, tempA)
            mstore(rootClaimOffset, tempB)
            mstore(pointerOffset, tempC)
        }
    }
}
