library CertsLib {
  struct SignatureData {
    /* 
     * status == 0x0 => UNKNOWN
     * status == 0x1 => PENDING
     * status == 0x2 => SIGNED
     * Otherwise     => Purpose (sha-256 of data)
     */
    bytes32 status;
    uint exp; // Expiration Date
  }

  struct TransferData {
    address newOwner;
    uint newEntityId;
  }

  struct CertData {
    /*
     * owner == 0 => POE_CERTIFICATE
     * owner != 0 => PROPIETARY_CERTIFICATE
     */
    address owner; // owner of the certificate (in case of being a peer)
    uint entityId; // owner of the certificate (in case of being an entity)
    bytes32 certHash; // sha256 checksum of the certificate JSON data
    string ipfsCertHash; // ipfs multihash address of certificate in json format
    bytes32 dataHash; // sha256 hash of certified data
    string ipfsDataHash; // ipfs multihash address of certified data
    mapping(uint => SignatureData) entities; // signatures from signing entities and their expiration date
    uint[] entitiesArr;
    mapping(address => SignatureData) signatures; // signatures from peers and their expiration date
    address[] signaturesArr;
  }

  struct Data {
    mapping(uint => CertData) certificates;
    mapping(uint => TransferData) transferRequests;
    uint nCerts;
  }

  // METHODS

  /**
   * Creates a new POE certificate
   * @param self {object} - The data containing the certificate mappings
   * @param dataHash {bytes32} - The hash of the certified data
   * @param certHash {bytes32} - The sha256 hash of the json certificate
   * @param ipfsDataHash {string} - The ipfs multihash address of the data (0x00 means unkwon)
   * @param ipfsCertHash {string} - The ipfs multihash address of the certificate in json format (0x00 means unkwon)
   * @return The id of the created certificate     
   */
  function createPOECertificate(Data storage self, bytes32 dataHash, bytes32 certHash, string ipfsDataHash, string ipfsCertHash) public returns (uint) {
    require (hasData(dataHash, certHash, ipfsDataHash, ipfsCertHash));

    uint certId = ++self.nCerts;
    self.certificates[certId] = CertData({
      owner: 0,
      entityId: 0,
      certHash: certHash,
      ipfsCertHash: ipfsCertHash,
      dataHash: dataHash,
      ipfsDataHash: ipfsDataHash,
      entitiesArr: new uint[](0),
      signaturesArr: new address[](0)
    });

    POECertificate(certId);
    return certId;
  }

  /**
   * Creates a new certificate (with known owner). The owner will be the sender unless the entityId (issuer) is supplied.
   * @param self {object} - The data containing the certificate mappings
   * @param ed {object} - The data containing the entity mappings
   * @param dataHash {bytes32} - The hash of the certified data
   * @param certHash {bytes32} - The sha256 hash of the json certificate
   * @param ipfsDataHash {string} - The ipfs multihash address of the data (0x00 means unkwon)
   * @param ipfsCertHash {string} - The ipfs multihash address of the certificate in json format (0x00 means unkwon)
   * @param entityId {uint} - The entity id which issues the certificate (0 if not issued by an entity)
   * @return {uint} The id of the created certificate     
   */
  function createCertificate(Data storage self, EntityLib.Data storage ed, bytes32 dataHash, bytes32 certHash, string ipfsDataHash, string ipfsCertHash, uint entityId) senderCanIssueEntityCerts(ed, entityId) public returns (uint) {
    require (hasData(dataHash, certHash, ipfsDataHash, ipfsCertHash));

    uint certId = ++self.nCerts;
    self.certificates[certId] = CertData({
      owner: entityId == 0 ? msg.sender : 0,
      entityId: entityId,
      certHash: certHash,
      ipfsCertHash: ipfsCertHash,
      dataHash: dataHash,
      ipfsDataHash: ipfsDataHash,
      entitiesArr: new uint[](0),
      signaturesArr: new address[](0)
    });

    Certificate(certId);
    return certId;
  }

  /**
   * Transfers a certificate owner. The owner can be a peer or an entity (never both), so only one of newOwner or newEntity must be different than 0.
   * If the specified certificateId belongs to an entity, the msg.sender must be a valid signer for the entity. Otherwise the msg.sender must be the current owner.
   * @param self {object} - The data containing the certificate mappings
   * @param ed {object} - The data containing the entity mappings
   * @param certificateId {uint} - The id of the certificate to transfer
   * @param newOwner {address} - The address of the new owner
   */
  function requestCertificateTransferToPeer(Data storage self, EntityLib.Data storage ed, uint certificateId, address newOwner) canTransferCertificate(self, ed, certificateId) public {
    self.transferRequests[certificateId] = TransferData({
      newOwner: newOwner,
      newEntityId: 0
    });

    CertificateTransferRequestedToPeer(certificateId, newOwner);
  }

  /**
   * Transfers a certificate owner. The owner can be a peer or an entity (never both), so only one of newOwner or newEntity must be different than 0.
   * If the specified certificateId belongs to an entity, the msg.sender must be a valid signer for the entity. Otherwise the msg.sender must be the current owner.
   * @param self {object} - The data containing the certificate mappings
   * @param ed {object} - The data containing the entity mappings
   * @param certificateId {uint} - The id of the certificate to transfer
   * @param newEntityId {uint} - The id of the new entity
   */
  function requestCertificateTransferToEntity(Data storage self, EntityLib.Data storage ed, uint certificateId, uint newEntityId) entityExists(ed, newEntityId) canTransferCertificate(self, ed, certificateId) public {
    self.transferRequests[certificateId] = TransferData({
      newOwner: 0,
      newEntityId: newEntityId
    });

    CertificateTransferRequestedToEntity(certificateId, newEntityId);
  }

  /**
   * Accept the certificate transfer
   * @param self {object} - The data containing the certificate mappings
   * @param ed {object} - The data containing the entity mappings
   * @param certificateId {uint} - The id of the certificate to transfer
   */
  function acceptCertificateTransfer(Data storage self, EntityLib.Data storage ed, uint certificateId) canAcceptTransfer(self, ed, certificateId) public {
    TransferData storage reqData = self.transferRequests[certificateId];
    self.certificates[certificateId].owner = reqData.newOwner;
    self.certificates[certificateId].entityId = reqData.newEntityId;    
    CertificateTransferAccepted(certificateId, reqData.newOwner, reqData.newEntityId);
    delete self.transferRequests[certificateId];
  }

  /**
   * Cancel any certificate transfer request
   * @param self {object} - The data containing the certificate mappings
   * @param ed {object} - The data containing the entity mappings
   * @param certificateId {uint} - The id of the certificate to transfer
   */
  function cancelCertificateTransfer(Data storage self, EntityLib.Data storage ed, uint certificateId) canTransferCertificate(self, ed, certificateId) public {
    self.transferRequests[certificateId] = TransferData({
      newOwner: 0,
      newEntityId: 0
    });

    CertificateTransferCancelled(certificateId);
  }

  /**
   * Updates ipfs multihashes of a particular certificate
   * @param self {object} - The data containing the certificate mappings
   * @param certId {uint} - The id of the certificate
   * @param ipfsDataHash {string} - The ipfs multihash address of the data (0x00 means unkwon)
   * @param ipfsCertHash {string} - The ipfs multihash address of the certificate in json format (0x00 means unkwon)
   */
  function setIPFSData(Data storage self, uint certId, string ipfsDataHash, string ipfsCertHash) ownsCertificate(self, certId) public {
      self.certificates[certId].ipfsDataHash = ipfsDataHash;
      self.certificates[certId].ipfsCertHash = ipfsCertHash;
      UpdatedIPFSData(certId);
  }

  // HELPERS

  /**
   * Returns true if the certificate has valid data
   * @param dataHash {bytes32} - The hash of the certified data
   * @param certHash {bytes32} - The sha256 hash of the json certificate
   * @param ipfsDataHash {string} - The ipfs multihash address of the data (0x00 means unkwon)
   * @param ipfsCertHash {string} - The ipfs multihash address of the certificate in json format (0x00 means unkwon)   * @return {bool} - True if the certificate contains valid data
   */
  function hasData(bytes32 dataHash, bytes32 certHash, string ipfsDataHash, string ipfsCertHash) pure public returns (bool) {
    return certHash != 0
    || dataHash != 0
    || bytes(ipfsDataHash).length != 0
    || bytes(ipfsCertHash).length != 0;
  }

  // MODIFIERS
  
 /**
   * Returns True if msg.sender is the owner of the specified certificate. False otherwise.
   * @param self {object} - The data containing the certificate mappings
   * @param id {uint} - The id of the certificate 
   */
  modifier ownsCertificate(Data storage self, uint id) {
    require (self.certificates[id].owner == msg.sender);
    _;
  }


  /**
   * Returns TRUE if the specified entity is valid and the sender is a valid signer from the entity.
   * If the entityId is 0 (not provided), it also returns TRUE
   * @param ed {object} - The data containing the entity mappings
   * @param entityId {uint} - The entityId which will issue the certificate
   */
  modifier senderCanIssueEntityCerts(EntityLib.Data storage ed, uint entityId) {
    require (entityId == 0 
     || (EntityLib.isValid(ed, entityId) && ed.entities[entityId].signers[msg.sender].status == 2));
    _;    
  }

  /**
   * Returns TRUE if the certificate has data and can be transfered to the new owner:
   * - When the certificate is owned by a peer: the sender must be the owner of the certificate
   * - When the certificate belongs to an entity: the entity must be valid 
   *   AND the signer must be a valid signer of the entity
   * @param self {object} - The data containing the certificate mappings
   * @param ed {object} - The data containing the entity mappings
   * @param certificateId {uint} - The certificateId which transfer is required
   */
  modifier canTransferCertificate(Data storage self, EntityLib.Data storage ed, uint certificateId) {
    CertData storage cert = self.certificates[certificateId];
    require (hasData(cert.dataHash, cert.certHash, cert.ipfsDataHash, cert.ipfsCertHash));

    if (cert.owner != 0) {
      require (cert.owner == msg.sender);
      _;
    } else if (cert.entityId != 0) {
      EntityLib.EntityData storage entity = ed.entities[cert.entityId];
      require (EntityLib.isValid(ed, cert.entityId) && entity.signers[msg.sender].status == 2);
      _;
    }
  }

  /**
   * Returns TRUE if the entity exists
   */
  modifier entityExists(EntityLib.Data storage ed, uint entityId) {
    require (EntityLib.exists(ed, entityId));
    _;
  }

  /**
   * Returns TRUE if the msg.sender can accept the certificate transfer
   * @param self {object} - The data containing the certificate mappings
   * @param ed {object} - The data containing the entity mappings
   * @param certificateId {uint} - The certificateId which transfer is required
   */
  modifier canAcceptTransfer(Data storage self, EntityLib.Data storage ed, uint certificateId) {
    CertData storage cert = self.certificates[certificateId];
    require (hasData(cert.dataHash, cert.certHash, cert.ipfsDataHash, cert.ipfsCertHash));

    TransferData storage reqData = self.transferRequests[certificateId];
    require(reqData.newEntityId != 0 || reqData.newOwner != 0);

    if (reqData.newOwner == msg.sender) {
      _;
    } else if (reqData.newEntityId != 0) {      
      EntityLib.EntityData storage newEntity = ed.entities[reqData.newEntityId];
      require (EntityLib.isValid(ed, reqData.newEntityId) && newEntity.signers[msg.sender].status == 2);
       _;
    }
  }

  // EVENTS

  event POECertificate(uint indexed certificateId);
  event Certificate(uint indexed certificateId);
  event CertificateTransferRequestedToPeer(uint indexed certificateId, address newOwner);
  event CertificateTransferRequestedToEntity(uint indexed certificateId, uint newEntityId);
  event CertificateTransferAccepted(uint indexed certificateId, address newOwner, uint newEntityId);
  event CertificateTransferCancelled(uint indexed certificateId);
  event UpdatedIPFSData(uint indexed certificateId);
}
function acceptCertificateTransfer(Data storage self, EntityLib.Data storage ed, uint certificateId) canAcceptTransfer(self, ed, certificateId) public {
  TransferData storage reqData = self.transferRequests[certificateId];
  self.certificates[certificateId].owner = reqData.newOwner;
  self.certificates[certificateId].entityId = reqData.newEntityId;    
  CertificateTransferAccepted(certificateId, reqData.newOwner, reqData.newEntityId);
  delete self.transferRequests[certificateId];
}

/**
 * Accept the certificate transfer
 * @param self {object} - The data containing the certificate mappings
 * @param ed {object} - The data containing the entity mappings
 * @param certificateId {uint} - The id of the certificate to transfer
 */