// File: src/FileSystem.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

import {SSTORE2} from "sstore2/SSTORE2.sol";

import {IContentStore} from "ethfs/packages/contracts/src/IContentStore.sol";
import {IFileSystem, Directory, File, Inode, InodeType} from "src/interfaces/IFileSystem.sol";

import "src/utils/Constants.sol";

/**
 * @title FileSystem
 * @author fx(hash)
 * @notice See the documentation in {IFileSystem}
 */
contract FileSystem is IFileSystem {
    /*//////////////////////////////////////////////////////////////////////////
                                    STORAGE
    //////////////////////////////////////////////////////////////////////////*/

    address internal constant GOERLI_CONTENT_STORE = 0x7c1730B7bE9424D0b983B84aEb254e3a2a105d91;
    address internal constant MAINNET_CONTENT_STORE = 0xC6806fd75745bB5F5B32ADa19963898155f9DB91;

    /**
     * @inheritdoc IFileSystem
     */
    address public immutable CONTENT_STORE;

    /**
     * @inheritdoc IFileSystem
     */
    mapping(bytes32 checksum => Inode inode) public inodes;

    /*//////////////////////////////////////////////////////////////////////////
                                  CONSTRUCTOR
    //////////////////////////////////////////////////////////////////////////*/

    /**
     * @dev Initializes the ContentStore contract
     */
    constructor() {
        CONTENT_STORE = block.chainid == 1 ? MAINNET_CONTENT_STORE : GOERLI_CONTENT_STORE;
    }

    /*//////////////////////////////////////////////////////////////////////////
                                  EXTERNAL FUNCTIONS
    //////////////////////////////////////////////////////////////////////////*/

    /**
     * @inheritdoc IFileSystem
     */
    function createDirectory(
        string[] calldata _fileNames,
        bytes32[] calldata _inodeChecksums
    ) external returns (bytes32 directoryChecksum) {
        if (_fileNames.length != _inodeChecksums.length) revert LengthMismatch();
        bytes memory concatenatedFiles = concatenateFiles(_fileNames, _inodeChecksums);

        for (uint256 i; i < _inodeChecksums.length; i++) {
            if (!inodeExists(_inodeChecksums[i])) revert InodeNotFound();
        }
        directoryChecksum = keccak256(bytes.concat(bytes1(uint8(InodeType.Directory)), concatenatedFiles));
        if (inodeExists(directoryChecksum)) return directoryChecksum;
        inodes[directoryChecksum].directory = Directory(_fileNames, _inodeChecksums);
        emit DirectoryCreated(directoryChecksum, _fileNames, _inodeChecksums);
    }

    /**
     * @inheritdoc IFileSystem
     */
    function createFile(
        bytes calldata _metadata,
        bytes32[] calldata _chunkPointers
    ) external returns (bytes32 fileChecksum) {
        for (uint256 i; i < _chunkPointers.length; i++) {
            if (!IContentStore(CONTENT_STORE).checksumExists(_chunkPointers[i])) revert ChunkNotFound();
        }
        fileChecksum = keccak256(
            bytes.concat(
                bytes1(uint8(InodeType.File)),
                keccak256(abi.encodePacked(_chunkPointers)),
                keccak256(_metadata)
            )
        );
        if (inodeExists(fileChecksum)) return fileChecksum;
        inodes[fileChecksum].inodeType = InodeType.File;
        inodes[fileChecksum].file = File(_metadata, _chunkPointers);
        emit FileCreated(fileChecksum, _metadata, _chunkPointers);
    }

    /**
     * @inheritdoc IFileSystem
     */
    function readDirectory(bytes32 _checksum) external view returns (string[] memory, bytes32[] memory) {
        if (!inodeExists(_checksum)) revert InodeNotFound();
        Inode memory inode = inodes[_checksum];
        if (inode.inodeType != InodeType.Directory) revert DirectoryNotFound();
        return (inode.directory.filenames, inode.directory.fileChecksums);
    }

    /**
     * @inheritdoc IFileSystem
     */
    function readFile(bytes32 _checksum) external view returns (bytes memory) {
        if (!inodeExists(_checksum)) revert InodeNotFound();
        Inode memory inode = inodes[_checksum];
        if (inode.inodeType != InodeType.File) revert FileNotFound();
        return concatenateChunks(inode.file.chunkChecksums);
    }

    /*//////////////////////////////////////////////////////////////////////////
                                  PUBLIC FUNCTIONS
    //////////////////////////////////////////////////////////////////////////*/

    /**
     * @inheritdoc IFileSystem
     */
    function concatenateChunks(bytes32[] memory _pointers) public view returns (bytes memory fileContent) {
        address pointer;
        bytes memory chunkContent;
        for (uint256 i; i < _pointers.length; i++) {
            pointer = IContentStore(CONTENT_STORE).getPointer(_pointers[i]);
            chunkContent = SSTORE2.read(pointer);
            fileContent = abi.encodePacked(fileContent, chunkContent);
        }
    }

    /**
     * @inheritdoc IFileSystem
     */
    function concatenateFiles(
        string[] calldata _fileNames,
        bytes32[] calldata _filePointers
    ) public pure returns (bytes memory concatenatedFiles) {
        uint256 length = _fileNames.length;
        bytes memory filename;
        for (uint256 i; i < length; i++) {
            filename = bytes(_fileNames[i]);
            if (filename.length == 0) revert InvalidFileName();
            if (_containsForbiddenChars(filename)) revert InvalidCharacter();
            concatenatedFiles = abi.encodePacked(_filePointers[i], keccak256(filename), concatenatedFiles);
        }
    }

    /**
     * @inheritdoc IFileSystem
     */
    function inodeExists(bytes32 _checksum) public view returns (bool) {
        Inode memory inode = inodes[_checksum];
        if (inode.inodeType == InodeType.File) {
            return inode.file.metadata.length != 0 || inode.file.chunkChecksums.length != 0;
        } else {
            return inode.directory.filenames.length != 0 || inode.directory.fileChecksums.length != 0;
        }
    }

    /*//////////////////////////////////////////////////////////////////////////
                                  PRIVATE FUNCTIONS
    //////////////////////////////////////////////////////////////////////////*/

    /**
     * @dev Checks if the given string contains any forbidden characters
     */
    function _containsForbiddenChars(bytes memory _string) private pure returns (bool) {
        uint256 length = _string.length;
        for (uint256 i; i < length; i++) {
            for (uint256 j; j < FORBIDDEN_CHARS.length; j++) {
                if (_string[i] == FORBIDDEN_CHARS[j]) {
                    return true;
                }
            }
        }
        return false;
    }

    function getInodeAt(
        bytes32 _inodeChecksum,
        string[] memory _pathSegments
    ) public view returns (bytes32, Inode memory) {
        if (!inodeExists(_inodeChecksum)) revert InodeNotFound();

        Inode memory inode = inodes[_inodeChecksum];
        bytes32 inodeChecksum = _inodeChecksum;

        uint256 length = _pathSegments.length;
        Directory memory directory;
        string[] memory filenames;
        bool found;
        for (uint256 i; i < length; i++) {
            if (inode.inodeType != InodeType.Directory) revert InodeNotFound();
            directory = inode.directory;
            filenames = inode.directory.filenames;
            found = false;
            for (uint256 j; j < filenames.length; j++) {
                if (keccak256(bytes(filenames[j])) == keccak256(bytes(_pathSegments[i]))) {
                    found = true;
                    inodeChecksum = directory.fileChecksums[j];
                    inode = inodes[inodeChecksum];
                    break;
                }
            }
            if (!found) revert InodeNotFound();
        }
        return (inodeChecksum, inode);
    }
}

// File: lib/sstore2/contracts/SSTORE2.sol



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

import "./utils/Bytecode.sol";

/**
  @title A key-value storage with auto-generated keys for storing chunks of data with a lower write & read cost.
  @author Agustin Aguilar <aa@horizon.io>

  Readme: https://github.com/0xsequence/sstore2#readme
*/
library SSTORE2 {
  error WriteError();

  /**
    @notice Stores `_data` and returns `pointer` as key for later retrieval
    @dev The pointer is a contract address with `_data` as code
    @param _data to be written
    @return pointer Pointer to the written `_data`
  */
  function write(bytes memory _data) internal returns (address pointer) {
    // Append 00 to _data so contract can't be called
    // Build init code
    bytes memory code = Bytecode.creationCodeFor(
      abi.encodePacked(
        hex'00',
        _data
      )
    );

    // Deploy contract using create
    assembly { pointer := create(0, add(code, 32), mload(code)) }

    // Address MUST be non-zero
    if (pointer == address(0)) revert WriteError();
  }

  /**
    @notice Reads the contents of the `_pointer` code as data, skips the first byte 
    @dev The function is intended for reading pointers generated by `write`
    @param _pointer to be read
    @return data read from `_pointer` contract
  */
  function read(address _pointer) internal view returns (bytes memory) {
    return Bytecode.codeAt(_pointer, 1, type(uint256).max);
  }

  /**
    @notice Reads the contents of the `_pointer` code as data, skips the first byte 
    @dev The function is intended for reading pointers generated by `write`
    @param _pointer to be read
    @param _start number of bytes to skip
    @return data read from `_pointer` contract
  */
  function read(address _pointer, uint256 _start) internal view returns (bytes memory) {
    return Bytecode.codeAt(_pointer, _start + 1, type(uint256).max);
  }

  /**
    @notice Reads the contents of the `_pointer` code as data, skips the first byte 
    @dev The function is intended for reading pointers generated by `write`
    @param _pointer to be read
    @param _start number of bytes to skip
    @param _end index before which to end extraction
    @return data read from `_pointer` contract
  */
  function read(address _pointer, uint256 _start, uint256 _end) internal view returns (bytes memory) {
    return Bytecode.codeAt(_pointer, _start + 1, _end + 1);
  }
}

// File: lib/ethfs/packages/contracts/src/IContentStore.sol



// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.13;

interface IContentStore {
    event NewChecksum(bytes32 indexed checksum, uint256 contentSize);

    error ChecksumExists(bytes32 checksum);
    error ChecksumNotFound(bytes32 checksum);

    function pointers(bytes32 checksum)
        external
        view
        returns (address pointer);

    function checksumExists(bytes32 checksum) external view returns (bool);

    function contentLength(bytes32 checksum)
        external
        view
        returns (uint256 size);

    function addPointer(address pointer) external returns (bytes32 checksum);

    function addContent(bytes memory content)
        external
        returns (bytes32 checksum, address pointer);

    function getPointer(bytes32 checksum)
        external
        view
        returns (address pointer);
}

// File: src/interfaces/IFileSystem.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

import {Directory, File, Inode, InodeType} from "src/lib/Structs.sol";

/**
 * @title IFileSystem
 * @author fx(hash)
 * @notice System for storing and retrieving files onchain
 */
interface IFileSystem {
    /*//////////////////////////////////////////////////////////////////////////
                                  Events
    //////////////////////////////////////////////////////////////////////////*/

    /**
     * @notice Event emitted when creating a new file inode
     */
    event FileCreated(bytes32 indexed _checksum, bytes metadata, bytes32[] _chunkPointers);

    /**
     * @notice Event emitted when creating a new directory inode
     */
    event DirectoryCreated(bytes32 indexed _checksum, string[] _names, bytes32[] _inodeChecksums);

    /*//////////////////////////////////////////////////////////////////////////
                                  ERRORS
    //////////////////////////////////////////////////////////////////////////*/

    /**
     * @notice Error thrown when attempting to read chunk that does not exist
     */
    error ChunkNotFound();
    /**
     * @notice Error thrown when reading a directory that does not exist
     */
    error DirectoryNotFound();

    /**
     * @notice Error thrown when attempting to read a file that does not exist
     */
    error FileNotFound();

    /**
     * @notice Error thrown when attempting to read an inode that does not exist
     */
    error InodeNotFound();

    /**
     * @notice Error thrown when file name is empty
     */
    error InvalidFileName();

    /**
     * @notice Error thrown when a forbidden character is present
     */
    error InvalidCharacter();

    /**
     * @notice Error thrown when array lengths do not match
     */
    error LengthMismatch();

    /*//////////////////////////////////////////////////////////////////////////
                                  FUNCTIONS
    //////////////////////////////////////////////////////////////////////////*/

    /**
     * @notice Concatenates the content of file chunks from the given pointers
     * @param _chunkChecksums Checksums for the file chunks
     * @return Concatenated content of the file chunks
     */
    function concatenateChunks(bytes32[] memory _chunkChecksums) external view returns (bytes memory);

    /**
     * @notice Returns the address of the ContentStore contract
     */
    function CONTENT_STORE() external view returns (address);

    /**
     * @notice Creates a new directory with the given names and file inode pointers
     * @param _fileNames The names of the files in the directory
     * @param _fileChecksums Pointers to the file inodes in the directory
     */
    function createDirectory(
        string[] calldata _fileNames,
        bytes32[] calldata _fileChecksums
    ) external returns (bytes32 directoryChecksum);

    /**
     * @notice Creates a new file with the given metadata and chunk pointers
     * @param _metadata Metadata of the file
     * @param _chunkChecksums Checksums for chunks of the file
     */
    function createFile(
        bytes calldata _metadata,
        bytes32[] calldata _chunkChecksums
    ) external returns (bytes32 fileChecksum);

    function getInodeAt(
        bytes32 _inodeChecksum,
        string[] memory _pathSegments
    ) external view returns (bytes32, Inode memory);

    /**
     * @notice Hashes a list of file names in the directory
     * @param _fileNames List of file names
     * @param _inodeChecksums List of checksums for the inodes
     * @return The concatenated files
     */
    function concatenateFiles(
        string[] calldata _fileNames,
        bytes32[] calldata _inodeChecksums
    ) external view returns (bytes memory);

    /**
     * @notice Mapping of checksum pointer to Inode struct
     */
    function inodes(bytes32 _checksum) external view returns (InodeType, File memory, Directory memory);

    /**
     * @notice Checks if an inode with the given checksum exists
     * @param _checksum Checksum of the inode
     * @return Status of inode existence
     */
    function inodeExists(bytes32 _checksum) external view returns (bool);

    /**
     * @notice Reads the content of a directory with the given checksum
     * @param _checksum Checksum of the directory
     * @return Names and file inode pointers in the directory
     */
    function readDirectory(bytes32 _checksum) external view returns (string[] memory, bytes32[] memory);

    /**
     * @notice Reads the content of a file with the given checksum
     * @param _checksum Checksum of the file
     * @return Content of the file
     */
    function readFile(bytes32 _checksum) external view returns (bytes memory);
}

// File: src/utils/Constants.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

/*//////////////////////////////////////////////////////////////////////////
                                CONSTANTS
//////////////////////////////////////////////////////////////////////////*/

bytes constant FORBIDDEN_CHARS = ":/?#[]@!$&'()*+,;=";

// File: lib/sstore2/contracts/utils/Bytecode.sol



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


library Bytecode {
  error InvalidCodeAtRange(uint256 _size, uint256 _start, uint256 _end);

  /**
    @notice Generate a creation code that results on a contract with `_code` as bytecode
    @param _code The returning value of the resulting `creationCode`
    @return creationCode (constructor) for new contract
  */
  function creationCodeFor(bytes memory _code) internal pure returns (bytes memory) {
    /*
      0x00    0x63         0x63XXXXXX  PUSH4 _code.length  size
      0x01    0x80         0x80        DUP1                size size
      0x02    0x60         0x600e      PUSH1 14            14 size size
      0x03    0x60         0x6000      PUSH1 00            0 14 size size
      0x04    0x39         0x39        CODECOPY            size
      0x05    0x60         0x6000      PUSH1 00            0 size
      0x06    0xf3         0xf3        RETURN
      <CODE>
    */

    return abi.encodePacked(
      hex"63",
      uint32(_code.length),
      hex"80_60_0E_60_00_39_60_00_F3",
      _code
    );
  }

  /**
    @notice Returns the size of the code on a given address
    @param _addr Address that may or may not contain code
    @return size of the code on the given `_addr`
  */
  function codeSize(address _addr) internal view returns (uint256 size) {
    assembly { size := extcodesize(_addr) }
  }

  /**
    @notice Returns the code of a given address
    @dev It will fail if `_end < _start`
    @param _addr Address that may or may not contain code
    @param _start number of bytes of code to skip on read
    @param _end index before which to end extraction
    @return oCode read from `_addr` deployed bytecode

    Forked from: https://gist.github.com/KardanovIR/fe98661df9338c842b4a30306d507fbd
  */
  function codeAt(address _addr, uint256 _start, uint256 _end) internal view returns (bytes memory oCode) {
    uint256 csize = codeSize(_addr);
    if (csize == 0) return bytes("");

    if (_start > csize) return bytes("");
    if (_end < _start) revert InvalidCodeAtRange(csize, _start, _end); 

    unchecked {
      uint256 reqSize = _end - _start;
      uint256 maxSize = csize - _start;

      uint256 size = maxSize < reqSize ? maxSize : reqSize;

      assembly {
        // allocate output byte array - this could also be done without assembly
        // by using o_code = new bytes(size)
        oCode := mload(0x40)
        // new "memory end" including padding
        mstore(0x40, add(oCode, and(add(add(size, 0x20), 0x1f), not(0x1f))))
        // store length in memory
        mstore(oCode, size)
        // actually retrieve the code, this needs assembly
        extcodecopy(_addr, add(oCode, 0x20), _start, size)
      }
    }
  }
}

// File: src/lib/Structs.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

/*//////////////////////////////////////////////////////////////////////////
                                STRUCTS
//////////////////////////////////////////////////////////////////////////*/

enum InodeType {
    Directory,
    File
}

struct Directory {
    string[] filenames;
    bytes32[] fileChecksums;
}

struct File {
    bytes metadata;
    bytes32[] chunkChecksums;
}

struct Inode {
    InodeType inodeType;
    File file;
    Directory directory;
}