/**
 * @title Blockchain Charity
 * @author <https://github.com/01Joseph-Hwang10>
 * @dev 
 * IE421 팀플용 컨트랙트 implementation. 
 * 주로 [TheCrowdChain](<https://github.com/syedMSohaib/thecrowdchain>)을 참조했습니다.
 * 
 * `전체적 설명` 페이지 > 앱 작동 방식 섹션에 첨부한 다이어그램을 구현했습니다. 
 * 다이어그램과 다른 점은, Dapp admin account를 따로 두지 않고,
 * 대신 `Project` 컨트랙트의 `current` 변수에 기부자들의 모든 펀드를 모아둔 후,
 * `FundRequest` 컨트랙트를 통해 수혜자에게 펀드를 전송합니다.
 *
 * Note: 부족한 부분이 있다 싶으면 언제든지 수정해주세요.
 */

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

// Importing OpenZeppelin's SafeMath Implementation
import '@openzeppelin/contracts/utils/math/SafeMath.sol';

// 아래는 저희 블록체인 기반 기부 플랫폼을 이루게 될 핵심 컨트랙트입니다.
// 이 컨트랙트는 기부자와 수혜자를 관리하고, 기부금을 관리하는 프로젝트를 관리합니다.
contract BlockchainCharity {
    using SafeMath for uint256;

    // 프로젝트 목록
    Project[] private projects;

    // 프로젝트 생성 이벤트
    // 프로젝트가 생성되면 이를 프론트엔드(UI)에 반영할 수 있습니다.
    event ProjectCreated(
        address causeAddress,
        address creator,
        string title,
        string projectType,
        string desciption
    );

    // 프로젝트를 새로 생성하는 함수입니다.
    // 외부에서 호출될 수 있는 external 함수입니다.   
    function startProject(
        string calldata title, 
        string calldata projectType, 
        string calldata description
    ) external {
        // Creating an object for project contract
        Project newProject = new Project(
            payable(msg.sender), 
            title, 
            projectType, 
            description
        );

        // Push in projects array created earlier
        projects.push(newProject);

        // Emit ProjectCreated event
        emit ProjectCreated(
            address(newProject),
            msg.sender,
            title,
            projectType,
            description
        );
    }

    // 모든 프로젝트 목록을 가져오는 함수입니다.
    // 프론트엔드(UI)에서 프로젝트 목록을 보여줄 때 사용됩니다.
    function getAllProjects() external view returns(Project[] memory ) {
        return projects;
    }
}

// 아래는 프로젝트를 관리하는 컨트랙트입니다.
// 
// `donate` 함수를 통해 기부자가 기부금을 기부할 수 있습니다.
contract Project {
    using SafeMath for uint256;

    // 프로젝트 상태를 정의하는 enum입니다.
    //
    // pending: 모금 중임을 의미합니다.
    // completed: 모금이 완료되었음을 의미합니다.
    enum State {
        pending,
        completed
    }

    // 프로젝트 생성자
    address payable public creator;
    // 현재 모금된 금액
    uint256 public current;
    // 프로젝트 제목
    string public title;
    // 프로젝트 유형
    string public projectType;
    // 프로젝트 설명
    string public description;

    // 프로젝트 상태
    State public state = State.pending;
    
    // 기부자 목록
    mapping(address => uint) public donors;
    uint256 public totalDonors;

    // 펀드 요청 목록
    FundRequest[] public fundRequests;

    // 컨트랙트가 기부금을 받았을 때 생성되는 이벤트입니다.
    event donationReceived(address donor, uint amount, uint current);

    // 기부금이 전부 모이고 이 기부금이 수혜자에게 전달되었을 때 생성되는 이벤트입니다.
    event donationSentToTarget(address recipient);

    // check the current state via modifier
    modifier checkState(State state_) {
        require(state == state_);
        _;
    }

    // check if caller is creator via modifier
    modifier isCreator() {
        require(msg.sender == creator);
        _;
    }

    // Check if caller is FundRequest via modifier
    modifier isFundRequest() {
        for (uint i = 0; i < fundRequests.length; i++) {
            require(msg.sender == address(fundRequests[i]));
            _;
        }
    }

    constructor(
        address payable _projectStarter,
        string memory _projectTitle,
        string memory _projectType,
        string memory _projectDescription
    ) {
        creator = _projectStarter;
        title = _projectTitle;
        projectType = _projectType;
        description = _projectDescription;
        current = 0;
    }

    // **프로젝트에 기부하는 함수입니다.**
    function donate() external checkState(State.pending) payable {
        require(msg.sender != creator);
        donors[msg.sender] = donors[msg.sender].add(msg.value);
        totalDonors++;
        current = current.add(msg.value);
        //emit donationReceived event
        emit donationReceived(msg.sender, msg.value, current);
    }

    // FundRequest에서 70% 이상의 donor의 투표를 받으면 호출되는 함수입니다.
    // FundRequest에서 정하는 value를 수혜자에게 전달합니다.
    function payToTarget(uint amount) public isFundRequest {
        require(current - amount >= 0);
        current -= amount;
        creator.transfer(amount);
        emit donationSentToTarget(creator);
    }

    // 펀드 요청을 생성합니다
    function createFundRequest(
        uint256 _value,
        string memory _description
    ) public isCreator {
        FundRequest newFundRequest = new FundRequest(
            address(this),
            _value,
            _description
        );
        fundRequests.push(newFundRequest);
    }

    // 모금을 완료합니다
    function finalizeProject() public isCreator {
        state = State.completed;
    }
}

// 프로젝트 생성자가 펀드 요청을 생성할 때 생성되는 컨트랙트입니다.
contract FundRequest {
    enum State {
        pending,
        resolved,
        rejected
    }
    
    // FundRequest 생성자. Project의 address여야합니다.
    address public recipient;
    // 요청하는 금액
    uint256 public value;
    // 요청한 사유
    string public description;
    State public state;
    uint256 public approvalCount;
    mapping(address => bool) public approvals;

    constructor(
        address _recipient, 
        uint256 _value, 
        string memory _description
    ) {
        recipient = _recipient;
        value = _value;
        description = _description;
        state = State.pending;
        approvalCount = 0;
    }

    // FundRequest를 생성한 프로젝트를 반환하는 함수입니다.
    function getProject() private view returns(Project) {
        return Project(recipient);
    }

    // Check if callar is donor from Project via modifier
    modifier isDonor {
        require(getProject().donors(msg.sender) > 0);
        _;
    }

    modifier isPending {
        require(state == State.pending);
        _;
    }

    // 기부자가 FundRequest승인에 투표하는 함수입니다.
    function approve() public isDonor isPending {
        approvals[msg.sender] = true;
        approvalCount++;

        Project project = getProject();
        // 70% 이상의 기부자가 펀드 요청을 승인하면 펀드 요청을 완료합니다.
        uint256 approvalThreshold = (project.totalDonors() * 70) / 100;
        if (approvalCount >= approvalThreshold) {
            project.payToTarget(value);
            state = State.resolved;
        }

        if (approvalCount == project.totalDonors()) {
            state = State.rejected;
        }
    }
}