// File: Application.sol



pragma solidity ^0.5.0;

import "./OrganizationFactoryInterface.sol";

/**
 * @title Application
 * @dev Application contract for the backend
 */
contract Application {

    struct OrganizationOwner {
        address organizationAddress;
        address organizationOwner;
        bool isActive;
    }

    address public lotFactory;
    address public applicationOwner;
    address[] public organizations;
    address[] public organizationOwners;
    OrganizationFactoryInterface public organizationFactory;

    mapping(address => OrganizationOwner) public organizationOwnersMap;

    event OrganizationCreated (
        address organization,
        address owner,
        address lotFactory
    );

    event OrganizationRemoved (
        address organization,
        address owner
    );

    event DefaultLotFactoryChanged (
        address oldLotFactory,
        address newLotFactory
    );
    
    event OrganizationFactoryChanged (
        address oldOrganizationFactory,
        address newOrganizationFactory
    );
    
    modifier onlyOwnerAccess() {
        require(msg.sender == applicationOwner,  "Only Owner accessible");
        _;
    }

    constructor(address _organizationFactory, address _lotFactory) public {
        applicationOwner = msg.sender;
        organizationFactory = OrganizationFactoryInterface(_organizationFactory);
        lotFactory = _lotFactory;
    }

    /**
     * @dev Updates the default lotFactory contract used when creating new orgs
     */
    function setLotFactory(address _newLotFactory)
    public
    onlyOwnerAccess
    returns (address) {
        address _oldLotFactory = lotFactory;
        lotFactory = _newLotFactory;
        emit DefaultLotFactoryChanged(_oldLotFactory, lotFactory);
    }

    /**
     * @dev Updates the organizationFactory contract
     */
    function setOrganizationFactory(address _newOrgFactory)
    public
    onlyOwnerAccess
    returns (address) {
        address _oldOrgFactory = address(organizationFactory);
        organizationFactory = OrganizationFactoryInterface(_newOrgFactory);
        emit OrganizationFactoryChanged(_oldOrgFactory, address(organizationFactory));
    }

    /**
     * @dev Creates new organization smart contract for given organization owner
     */
    function createOrganization(
        address _organizationOwner
    )
    public
    onlyOwnerAccess
    returns (address)
    {
        return createOrganization(_organizationOwner,lotFactory);
    }

     /**
     * @dev Creates new organization smart contract for given organization owner
     */
    function createOrganization(
        address _organizationOwner,
        address _lotFactory
    )
    public
    onlyOwnerAccess
    returns (address)
    {
        address newOrgAddress = organizationFactory.createOrganization( _lotFactory,_organizationOwner, true);
        organizationOwners.push(_organizationOwner);
        organizations.push(newOrgAddress);
        organizationOwnersMap[_organizationOwner] = OrganizationOwner(
            newOrgAddress,
            _organizationOwner,
            true
        );

        emit OrganizationCreated(newOrgAddress, _organizationOwner, _lotFactory);

        return newOrgAddress;
    }

    /**
     * @dev Get all the Organizations list of addresses.
     */
    function getOrganizations()
    public
    view
    returns (address[] memory) {
        return organizations;
    }

    /**
     * @dev Remove organization so that they shouldn't create any more lots.
     * @param _organization The address of organization which needs to be removed.
     */
    function removeOrganization(address _organization)
    public
    onlyOwnerAccess
    {
        organizationOwnersMap[_organization].isActive = false;

        emit OrganizationRemoved(_organization, msg.sender);
    }

    /**
     * @dev Remove organization so that they shouldn't create any more lots.
     * @param _organization The address of organization which needs to be removed.
     */
    function getOrganization(address _organization)
    public view
    returns (address, bool)
    {
        return (organizationOwnersMap[_organization].organizationOwner, organizationOwnersMap[_organization].isActive);
    }
}

// File: Lot.sol



pragma solidity ^0.5.0;

import "./LotInterface.sol";

import "./OrganizationInterface.sol";
import "./PermissionsEnum.sol";

/**
 * @title Lot
 * @dev Lot contract to create new lot and change the states and change the ownership
 */
contract Lot is PermissionsEnum, LotInterface {
    address public factory;

    address public organization;
    address public parentLot;
    address public nextPermitted;

    string public infoFileHash;
    string public name;

    uint32 public totalSupply;
    uint32 public transferredSupply;

    mapping(address => uint32) public supplyDistributionInfo;

    enum LotState {
        NEW,
        INITIAL,
        GROW,
        HARVEST,
        EXTRACTING,
        EXTRACTED,
        TESTING,
        TESTED,
        PRODUCT,
        COMPLETE
    }

    LotState public state;

    event LotTotalSupplyConfigured (
        address organization,
        address lot,
        uint32 totalSupply
    );

    event LotNextPermittedChanged (
        address lot,
        address permitted
    );

    event LotStateChanged (
        address organization,
        address lot,
        uint previousState,
        uint nextState,
        string infoFileHash
    );

    event LotOwnershipTransferred (
        address lot,
        address currentOwner,
        address newOwner
    );

    modifier hasPermission(Permissions perm) {
        require(OrganizationInterface(organization).hasPermissions(msg.sender, uint256(perm)), "Not Allowed");
        _;
    }

    constructor(
        address _organization,
        address _factory,
        string memory _name,
        uint32 _totalSupply,
        address _parentLot,
        address _permitted)
    public {
        organization = _organization;
        factory = _factory;

        name = _name;
        totalSupply = _totalSupply;
        parentLot = _parentLot;
        initLot(0);

        nextPermitted = _permitted;
        if (_permitted != address(0)) {
            emit LotNextPermittedChanged(address(this), _permitted);
        }
    }

    function getOrganization() public view returns (address) {
        return organization;
    }

    function _getSubmittingOrganization(Permissions perm) internal view returns (address) {
        // check nextPermitted oranization
        if (nextPermitted != address(0) && OrganizationInterface(nextPermitted).hasPermissions(msg.sender, uint256(perm))) return nextPermitted;

        if (OrganizationInterface(organization).hasPermissions(msg.sender, uint256(perm))) return organization;

        return address(0);
    }

    /**
     * @dev Change the Lot State.
     * @param _nextState The next state of the Lot.
     * @param _infoFileHash The new infoFileHash of the Lot from IPFS .
     */
    function changeLotState(
        uint _nextState,
        string memory _infoFileHash
    )
    public
    {
        address submittingOrganization = _getSubmittingOrganization(Permissions.UPDATE_LOT);
        require(submittingOrganization != address(0), "Not Allowed");

        uint previousState = uint(state);
        require(_nextState != previousState, "Cannot submit the same state over");
        
        state = LotState(_nextState);
        infoFileHash = _infoFileHash;

        // Always return back to null after lot has changed
        nextPermitted = address(0);

        emit LotStateChanged(submittingOrganization, address(this), previousState, _nextState, _infoFileHash);
    }

    /**
     * @dev Changes the Lot state to Next State
     * @param _nextState The next Lot State.
     * @param _infoFileHash The File Hash representing IPFS record.
     * @param _permitted Next permitted user for the Lot.
     */
    function changeLotStateWithNextPermitted(
        uint _nextState,
        string memory _infoFileHash,
        address _permitted
    )
    public
    {
        address submittingOrganization = _getSubmittingOrganization(Permissions.UPDATE_LOT);
        require(submittingOrganization != address(0), "Not Allowed");

        uint previousState = uint(state);
        require(_nextState != previousState, "Cannot submit the same state over");

        state = LotState(_nextState);
        infoFileHash = _infoFileHash;

        nextPermitted = _permitted;

        emit LotStateChanged(submittingOrganization, address(this), previousState, _nextState, _infoFileHash);
        emit LotNextPermittedChanged(address(this), _permitted);
    }

    /**
     * @dev Sets the lot sate
     * @param _lotState The state of the lot.
     */
    function setLotState(uint _lotState)
    public
    {
        address submittingOrganization = _getSubmittingOrganization(Permissions.UPDATE_LOT);
        require(submittingOrganization != address(0), "Not Allowed");

        uint previousState = uint256(state);

        state = LotState(_lotState);

        emit LotStateChanged(submittingOrganization, address(this), previousState, _lotState, infoFileHash);
    }

    /**
     * @dev Allows to set infoHas to current Lot IPFS.
     * @param _infoFileHash The File Hash representing IPFS record.
     */
    function setInfoFileHash(string memory _infoFileHash)
    public
    {
        address submittingOrganization = _getSubmittingOrganization(Permissions.UPDATE_LOT);
        require(submittingOrganization != address(0), "Not Allowed");

        infoFileHash = _infoFileHash;
    }

    /**
     * @dev Allows to set totalSupply to current Lot.
     * @param _totalSupply The total supply of the current lot.
     */
    function setTotalSupply(uint32 _totalSupply)
    public
    hasPermission(Permissions.UPDATE_LOT)
    {
        require(totalSupply == 0, "Total supply already set");

        totalSupply = _totalSupply;

        emit LotTotalSupplyConfigured(organization, address(this), _totalSupply);
    }

    /**
     * @dev Sets the next permitted address to do a state change on Lot
     * @param _permitted the address to be added
     */
    function setNextPermitted(address _permitted)
    public
    hasPermission(Permissions.UPDATE_LOT)
    {
        nextPermitted = _permitted;
        emit LotNextPermittedChanged(address(this), _permitted);
    }

    /**
     * @dev Allows the current owner to transfer control of the contract to a newOwner.
     * @param _newOwner The address to transfer ownership to.
     */
    function transferOwnership(address _newOwner)
    public
    hasPermission(Permissions.TRANSFER_LOT_OWNERSHIP)
    returns (bool)
    {
        address currentOwner = organization;
        organization = _newOwner;

        emit LotOwnershipTransferred(address(this), currentOwner, _newOwner);
        return true;
    }

    /**
     * @dev Retieve the File Hash.
     */
    function retrieveFileHash()
    public
    view
    returns (string memory)
    {
        return infoFileHash;
    }

    /**
     * @dev Retieve Lot state.
     */
    function retrieveState()
    public
    view
    returns (uint)
    {
        return uint(state);
    }

    /**
     * @dev Retieve Total Supply.
     */
    function retrieveTotalSupply()
    public
    view
    returns (uint32)
    {
        return totalSupply;
    }

    /**
     * @dev Retieve Total Supply.
     */
    function retrieveTransferredSupply()
    public
    view
    returns (uint32)
    {
        return transferredSupply;
    }

    /**
     * @dev Retieve Sub Lot Supply.
     * @param _lotAddress The address to Sub Lot.
     */
    function retrieveSubLotSupply(address _lotAddress)
    public
    view
    returns (uint32)
    {
        return supplyDistributionInfo[_lotAddress];
    }

    /**
     * @dev Creates a new Lot.
     * @param _lotState The current state of the Lot.
     */
    function initLot(
        uint _lotState
    )
    private
    {
        state = LotState(_lotState);
    }

    /**
     * @dev Allocates supply for a given lot address
     * @param _quantity The total supply transferred
     */
    function allocateSupply(address _lotAddress, uint32 _quantity)
    public
    hasPermission(Permissions.ALLOCATE_SUPPLY)
    {
        require((transferredSupply + _quantity) <= totalSupply, "Cannot allocate supply, exceeds total supply");
        supplyDistributionInfo[_lotAddress] = _quantity;
        transferredSupply += _quantity;
    }
}

// File: LotFactory.sol



pragma solidity ^0.5.0;

import "./LotFactoryInterface.sol";
import "./LotInterface.sol";
import "./OrganizationInterface.sol";
import "./PermissionsEnum.sol";

import "./Lot.sol";

contract LotFactory is PermissionsEnum, LotFactoryInterface {
    event LotCreated (
        address organization,
        address lot,
        string name
    );

    event SubLotCreated (
        address organization,
        address lot,
        address parentLot,
        string name,
        uint32 totalSupply
    );

    function createLot(
        address _organization,
        string memory _name)
    public 
    returns (address) 
    {
        require(OrganizationInterface(_organization).hasPermissions(msg.sender, uint256(Permissions.CREATE_LOT)), "Not Allowed");

        address lot = _createLot(_organization, _name, 0, address(0), address(0));

        emit LotCreated(_organization, lot, _name);
        return lot;
    }

    function createSubLot(
        address _organization,
        address _parentLot,
        string memory _name,
        uint32 _totalSupply,
        address _nextPermitted)
    public
    returns (address)
    {
        require(OrganizationInterface(_organization).hasPermissions(msg.sender, uint256(Permissions.CREATE_SUB_LOT)), "Not Allowed");
        require(LotInterface(_parentLot).getOrganization() == _organization, "Lot does not belong to the organization");
        require(_totalSupply > 0, "Total supply must be greater than 0");

        address lot = _createLot(_organization, _name, _totalSupply, _parentLot, _nextPermitted);
        LotInterface(_parentLot).allocateSupply(lot, _totalSupply);

        emit SubLotCreated(_organization, lot, _parentLot, _name, _totalSupply);
        return lot;
    }

    function _createLot(
        address _organization,
        string memory _name,
        uint32 _totalSupply,
        address _parentLot,
        address _nextPermitted
    ) internal returns (address) {
        return address (new Lot(_organization, address(this), _name, _totalSupply, _parentLot, _nextPermitted));
    }
}

// File: LotFactoryInterface.sol



pragma solidity ^0.5.0;

contract LotFactoryInterface {
    function createLot(
        address _organization,
        string memory _name)
    public
    returns(
        address);

    /*
     * @dev Create Sub Lot for existing Lot.
     */
    function createSubLot(
        address _organization,
        address _parentLot,
        string memory _name,
        uint32 _totalSupply,
        address _nextPermitted)
    public
    returns (
        address);
}

// File: LotInterface.sol



pragma solidity ^0.5.0;

contract LotInterface {
    function getOrganization() public view returns (address);
    function allocateSupply(address _lotAddress, uint32 _quantity) public;
}

// File: Migrations.sol



pragma solidity ^0.5.0;

/**
 * @title Migrations
 *
 *
 */
contract Migrations {
    address public owner;
    uint public last_completed_migration;

    modifier restricted() {
        if (msg.sender == owner) _;
    }

    constructor() public {
        owner = msg.sender;
    }

    function setCompleted(uint completed) public restricted {
        last_completed_migration = completed;
    }

    function upgrade(address new_address) public restricted {
        Migrations upgraded = Migrations(new_address);
        upgraded.setCompleted(last_completed_migration);
    }
}

// File: Organization.sol



pragma solidity ^0.5.0;

import "./OrganizationInterface.sol";
import "./PermissionsEnum.sol";


/**
 * @title Organization
 * Organization contract to create new organization, add/delete Devices,
 * create new Lot
 */
contract Organization is OrganizationInterface, PermissionsEnum {

    address public lotFactory;

    mapping(address => bool) public adminDevices;

    mapping(address => bool) public permittedDevices;

    bool public isActive;

    event DeviceAdded (
        address device
    );

    event DeviceRemoved (
        address device
    );

    event AdminDeviceAdded (
        address device
    );

    event AdminDeviceRemoved (
        address device
    );

    event LotFactoryChanged (
        address oldFactory,
        address newFactory
    );

    modifier onlyOwnerAccess() {
        require(isAdminDevice(msg.sender), "Only admin device accesible");
        _;
    }

    modifier onlyPermittedAccess() {
        require(isPermittedDevice(msg.sender), "Only permitted device accesible");
        _;
    }

    constructor(
        address _lotFactory,
        address _organizationOwner,
        bool _isActive
    )
    public {
        lotFactory = _lotFactory;

        // Set all the roles to false initially
        isActive = _isActive;

        // make admin the owner device
        _permitAdminDevice(_organizationOwner);
    }
    
    /**
     * @dev Returns the organization info
     */
    function organizationInfo()
    public
    view
    returns (bool status) {
        status = isActive;
    }

    /**
     * @dev Check if device is able to add or remove permitted devices.
     * @param _device The address to device check access.
     */
    function isAdminDevice(address _device)
    public
    view
    returns (bool deviceAdmin)
    {
        return adminDevices[_device];
    }

    /**
     * @dev Check if device is Permitted to perform Lot related operations.
     * @param _device The address to device check access.
     */
    function isPermittedDevice(address _device)
    public
    view
    returns (bool devicePermitted)
    {
        return (permittedDevices[_device] || adminDevices[_device]);
    }

    /**
    * @dev Update lot factory address
    */
    function setLotFactory(address _lotFactory)
    public
    onlyOwnerAccess
    {
        address oldFactory = lotFactory;
        lotFactory = _lotFactory;

        // Emit we updated lot factory
        emit LotFactoryChanged(oldFactory, _lotFactory);
    }

    /**
     * @dev Allows the new _device to access organization.
     * @param _deviceAddress The address of new device which needs access to organization.
     */
    function permitDevice(address _deviceAddress)
    public
    onlyOwnerAccess
    returns (bool devicePermitted)
    {
        return _permitDevice(_deviceAddress);
    }

    function removeDevice(address _deviceAddress)
    public
    onlyOwnerAccess
    returns (bool devicePermitted)
    {
        return _removeDevice(_deviceAddress);
    }

    function _permitDevice(address _deviceAddress)
    private
    returns (bool devicePermitted)
    {
        //validation to check already exist in the list
        permittedDevices[_deviceAddress] = true;

        emit DeviceAdded(_deviceAddress);
        return true;
    }

    function _removeDevice(address _deviceAddress)
    private

    returns (bool devicePermitted)
    {
        permittedDevices[_deviceAddress] = false;

        emit DeviceRemoved(_deviceAddress);
        return true;
    }

    function permitAdminDevice(address _deviceAddress)
    public
    onlyOwnerAccess
    returns (bool deviceAdmin)
    {
        return _permitAdminDevice(_deviceAddress);
    }

    function removeAdminDevice(address _deviceAddress)
    public
    onlyOwnerAccess
    returns (bool deviceAdmin)
    {
        return _removeAdminDevice(_deviceAddress);
    }

    function _permitAdminDevice(address _deviceAddress)
    private
    returns (bool deviceAdmin)
    {
        //validation to check already exist in the list
        adminDevices[_deviceAddress] = true;
        permittedDevices[_deviceAddress] = true;

        emit AdminDeviceAdded(_deviceAddress);
        return true;
    }

    function _removeAdminDevice(address _deviceAddress)
    private
    returns (bool deviceAdmin)
    {
        adminDevices[_deviceAddress] = false;
        permittedDevices[_deviceAddress] = false;

        emit AdminDeviceRemoved(_deviceAddress);
        return true;
    }
    

    function hasPermissions(address permittee, uint256 permission)
    public
    view
    returns (bool)
    {
        if (permittee == address(this)) return true;

        if (permission == uint256(Permissions.CREATE_LOT)) return isPermittedDevice(permittee);
        if (permission == uint256(Permissions.CREATE_SUB_LOT)) return isPermittedDevice(permittee);
        if (permission == uint256(Permissions.UPDATE_LOT)) return isPermittedDevice(permittee);
        if (permission == uint256(Permissions.TRANSFER_LOT_OWNERSHIP)) return isPermittedDevice(permittee);
        if (permission == uint256(Permissions.ALLOCATE_SUPPLY)) return permittee == address(lotFactory);

        return false;
    }
}

// File: OrganizationFactory.sol




pragma solidity ^0.5.0;

import "./OrganizationFactoryInterface.sol";
import "./Organization.sol";

contract OrganizationFactory is OrganizationFactoryInterface {

    function createOrganization(address _lotFactory, address _organizationOwner, bool _isActive) public returns (address) {
        Organization organization = new Organization( _lotFactory, _organizationOwner, _isActive);
        return address(organization);
    }
} 

// File: OrganizationFactoryInterface.sol



pragma solidity ^0.5.0;

interface OrganizationFactoryInterface {
    function createOrganization(address lotFactory, address owner, bool isActive) external returns (address);
}
// File: OrganizationInterface.sol



pragma solidity ^0.5.0;

contract OrganizationInterface {
    function hasPermissions(address permittee, uint256 permission) public view returns (bool);
}

// File: PermissionsEnum.sol



pragma solidity ^0.5.0;

contract PermissionsEnum {
    enum Permissions {
        CREATE_LOT,
        CREATE_SUB_LOT,
        UPDATE_LOT,
        TRANSFER_LOT_OWNERSHIP,
        ALLOCATE_SUPPLY
    }
}