contract AssetsStorage is ResolverClient, IndexedBytesIteratorStorage, BytesIteratorStorage, DigixConstants {

  using StateMachine for StateMachine.System;
  using DoublyLinkedList for DoublyLinkedList.IndexedBytes;
  using DoublyLinkedList for DoublyLinkedList.Bytes;

  struct Item {
    uint256 product_id;
    uint256 ng_weight;
    uint256 effective_ng_weight;
    bytes32 serial;
    uint256 time_minted;
    uint256 redeem_deadline;
    address mint_target;
    address redeem_for;
    bytes32 replaced_by; // id of the item that replaces this item if it fails audit
    bytes32 replaces; // id of the item that this item replaces
  }

  struct System {
    StateMachine.System state_machine;
    DoublyLinkedList.Bytes global_audit_documents;
    DoublyLinkedList.IndexedBytes asset_documents;
    DoublyLinkedList.IndexedBytes user_recasts;
    mapping (bytes32 => Item) items_by_id;
    uint256 last_global_audit_time;
    uint256 redeem_period;              // Recaster has to redeem the asset within redeem_period days
    // last_asset_with_serial[serial_no] is the asset id of the latest asset that was added into the system with that serial_no
    // note that a gold bar with the same serial number can go through multiple cycles of minting and redeeming
    // and last_asset_with_serial records the asset id in the last cycle
    mapping (bytes32 => bytes32) last_asset_with_serial;
  }

  System system;

  function AssetsStorage(address _resolver) public
  {
    require(init(CONTRACT_STORAGE_ASSETS, _resolver));
    require(system.state_machine.init());
    system.redeem_period = 31; // default to 31 days

    // roles
    system.state_machine.set_role_name(ROLE_VENDOR, "vendor");
    system.state_machine.set_role_name(ROLE_XFERAUTH, "xferauth");
    system.state_machine.set_role_name(ROLE_POPADMIN, "popadmin");
    system.state_machine.set_role_name(ROLE_CUSTODIAN, "custodian");
    system.state_machine.set_role_name(ROLE_AUDITOR, "auditor");

    // state names
    system.state_machine.set_state_name(STATE_CREATED, "created");
    system.state_machine.set_state_name(STATE_VENDOR_ORDER, "vendor_order");
    system.state_machine.set_state_name(STATE_TRANSFER, "transfer");
    system.state_machine.set_state_name(STATE_CUSTODIAN_DELIVERY, "custodian_delivery");
    system.state_machine.set_state_name(STATE_MINTED, "minted");
    system.state_machine.set_state_name(STATE_AUDIT_FAILURE, "audit_failure");
    system.state_machine.set_state_name(STATE_REPLACEMENT_ORDER, "replacement_order");
    system.state_machine.set_state_name(STATE_REPLACEMENT_DELIVERY, "replacement_delivery");
    system.state_machine.set_state_name(STATE_RECASTED, "recasted");
    system.state_machine.set_state_name(STATE_REDEEMED, "redeemed");
    system.state_machine.set_state_name(STATE_ADMIN_FAILURE, "admin_failure");

    // popadmin: create an asset item
    system.state_machine.grant_access(ROLE_POPADMIN, STATE_ZERO_UNDEFINED, STATE_CREATED);

    // popadmin: change a newly created asset to 'vendor_order'
    system.state_machine.grant_access(ROLE_POPADMIN, STATE_CREATED, STATE_VENDOR_ORDER);

    // popadmin: change a newly created asset to 'transfer'
    system.state_machine.grant_access(ROLE_POPADMIN, STATE_CREATED, STATE_TRANSFER);

    // vendor: vendor_order to custodian_delivery
    system.state_machine.grant_access(ROLE_VENDOR, STATE_VENDOR_ORDER, STATE_CUSTODIAN_DELIVERY);

    // xferauth: transfer to custodian_delivery
    system.state_machine.grant_access(ROLE_XFERAUTH, STATE_TRANSFER, STATE_CUSTODIAN_DELIVERY);

    // custodian: custodian_delivery to minted
    system.state_machine.grant_access(ROLE_CUSTODIAN, STATE_CUSTODIAN_DELIVERY, STATE_MINTED);

    // auditor: minted to audit_failure
    system.state_machine.grant_access(ROLE_AUDITOR, STATE_MINTED, STATE_AUDIT_FAILURE);

    // popadmin: created to replacement_order
    system.state_machine.grant_access(ROLE_POPADMIN, STATE_CREATED, STATE_REPLACEMENT_ORDER);

    // vendor: replacement_order to replacement_delivery
    system.state_machine.grant_access(ROLE_VENDOR, STATE_REPLACEMENT_ORDER, STATE_REPLACEMENT_DELIVERY);

    // popadmin: vendor_order to admin_failure
    system.state_machine.grant_access(ROLE_POPADMIN, STATE_VENDOR_ORDER, STATE_ADMIN_FAILURE);

    // popadmin: transfer to admin_failure
    system.state_machine.grant_access(ROLE_POPADMIN, STATE_TRANSFER, STATE_ADMIN_FAILURE);

    // popadmin: custodian_delivery to admin_failure
    system.state_machine.grant_access(ROLE_POPADMIN, STATE_CUSTODIAN_DELIVERY, STATE_ADMIN_FAILURE);

    // popadmin: replacement_order to admin_failure
    system.state_machine.grant_access(ROLE_POPADMIN, STATE_REPLACEMENT_ORDER, STATE_ADMIN_FAILURE);

    // popadmin: replacement_delivery to admin_failure
    system.state_machine.grant_access(ROLE_POPADMIN, STATE_REPLACEMENT_DELIVERY, STATE_ADMIN_FAILURE);

    // custodian: replacement_delivery to minted
    system.state_machine.grant_access(ROLE_CUSTODIAN, STATE_REPLACEMENT_DELIVERY, STATE_MINTED);

    // any: minted to recasted
    system.state_machine.grant_access(ROLE_ZERO_ANYONE, STATE_MINTED, STATE_RECASTED);

    // popadmin: recasted to redeemed
    system.state_machine.grant_access(ROLE_POPADMIN, STATE_RECASTED, STATE_REDEEMED);

    // popadmin: recasted to minted
    system.state_machine.grant_access(ROLE_POPADMIN, STATE_RECASTED, STATE_MINTED);

    // kycrecaster: recasted to minted (only if reminted by recaster)
    system.state_machine.grant_access(ROLE_KYC_RECASTER, STATE_RECASTED, STATE_MINTED);
  }

  function read_asset_info(bytes32 _item)
           public
           constant
           returns (uint256 _product_id, uint256 _ng_weight, uint256 _effective_ng_weight,
                    bytes32 _serial, uint256 _state_id, uint256 _documents_count, uint256 _time_minted, uint256 _redeem_deadline)
  {
    _product_id = system.items_by_id[_item].product_id;
    _ng_weight = system.items_by_id[_item].ng_weight;
    _effective_ng_weight = system.items_by_id[_item].effective_ng_weight;
    _serial = system.items_by_id[_item].serial;
    _state_id = system.state_machine.get_item_state_id(_item);
    _documents_count = read_total_asset_documents(_item);
    _time_minted = system.items_by_id[_item].time_minted;
    _redeem_deadline = system.items_by_id[_item].redeem_deadline;
  }

  function read_last_asset_with_serial(bytes32 _serial)
           public
           constant
           returns (bytes32 _asset_id)
  {
    _asset_id = system.last_asset_with_serial[_serial];
  }

  function read_asset_state(bytes32 _item)
           public
           constant
           returns (uint256 _state_id)
  {
    _state_id = system.state_machine.get_item_state_id(_item);
  }

  function read_redeem_period()
           public
           constant
           returns (uint256 _redeem_period)
  {
    _redeem_period = system.redeem_period;
  }

  function read_asset_details(bytes32 _item)
           public
           constant
           returns (address _mint_target, address _redeem_for, bytes32 _replaced_by, bytes32 _replaces)
  {
    _mint_target = system.items_by_id[_item].mint_target;
    _redeem_for = system.items_by_id[_item].redeem_for;
    _replaced_by = system.items_by_id[_item].replaced_by;
    _replaces = system.items_by_id[_item].replaces;
  }

  function read_last_global_audit_time()
           public
           constant
           returns (uint256 _last_global_audit_time)
  {
    _last_global_audit_time = system.last_global_audit_time;
  }

  function read_state_name(uint256 _state_id)
           public
           constant
           returns (bytes32 _state_name)
  {
    _state_name = system.state_machine.get_state_name(_state_id);
  }

  function read_item_state_name(bytes32 _item)
           public
           constant
           returns (bytes32 _state_name)
  {
    _state_name = read_state_name(system.state_machine.get_item_state_id(_item));
  }

  function read_first_item_in_state(bytes32 _state_id)
           public
           constant
           returns (bytes32 _item)
  {
    _item = system.state_machine.get_first_in_state(uint256(_state_id));
  }

  function read_last_item_in_state(bytes32 _state_id)
           public
           constant
           returns (bytes32 _item)
  {
    _item = system.state_machine.get_last_in_state(uint256(_state_id));
  }

  function read_next_item_from_item_in_state(bytes32 _state_id, bytes32 _current_item)
           public
           constant
           returns (bytes32 _item)
  {
    _item = system.state_machine.get_next_from_in_state(uint256(_state_id), _current_item);
  }

  function read_previous_item_from_item_in_state(bytes32 _state_id, bytes32 _current_item)
           public
           constant
           returns (bytes32 _item)
  {
    _item = system.state_machine.get_previous_from_in_state(uint256(_state_id), _current_item);
  }

  function read_total_items_in_state(bytes32 _state_id)
           public
           constant
           returns (uint256 _total_items)
  {
    _total_items = system.state_machine.total_in_state(uint256(_state_id));
  }


  function read_total_asset_documents(bytes32 _item)
           public
           constant
           returns (uint256 _total_count)
  {
    _total_count = read_total_indexed_bytesarray(system.asset_documents, _item);
  }

  function read_first_asset_document(bytes32 _item)
           public
           constant
           returns (bytes32 _document)
  {
    _document = read_first_from_indexed_bytesarray(system.asset_documents, _item);
  }

  function read_last_asset_document(bytes32 _item)
           public
           constant
           returns (bytes32 _document)
  {
    _document = read_last_from_indexed_bytesarray(system.asset_documents, _item);
  }

  function read_next_asset_document_from_document(bytes32 _item, bytes32 _current_document)
           public
           constant
           returns (bytes32 _document)
  {
    _document = read_next_from_indexed_bytesarray(system.asset_documents, _item, _current_document);
  }


  function read_previous_asset_document_from_document(bytes32 _item, bytes32 _current_document)
           public
           constant
           returns (bytes32 _document)
  {
    _document = read_previous_from_indexed_bytesarray(system.asset_documents, _item, _current_document);
  }

  function read_first_global_audit()
           public
           constant
           returns (bytes32 _document)
  {
    _document = read_first_from_bytesarray(system.global_audit_documents);
  }

  function read_last_global_audit()
           public
           constant
           returns (bytes32 _document)
  {
    _document = read_last_from_bytesarray(system.global_audit_documents);
  }

  function read_next_global_audit_from_audit(bytes32 _current_document)
           public
           constant
           returns (bytes32 _document)
  {
    _document = read_next_from_bytesarray(system.global_audit_documents, _current_document);
  }


  function read_previous_global_audit_from_audit(bytes32 _current_document)
           public
           constant
           returns (bytes32 _document)
  {
    _document = read_previous_from_bytesarray(system.global_audit_documents, _current_document);
  }

  function read_total_global_audits()
           public
           constant
           returns (uint256 _total_count)
  {
    _total_count = system.global_audit_documents.total();
  }

  function read_user_first_recast(bytes32 _user)
           public
           constant
           returns (bytes32 _item)
  {
    _item = read_first_from_indexed_bytesarray(system.user_recasts, _user);
  }

  function read_user_last_recast(bytes32 _user)
           public
           constant
           returns (bytes32 _item)
  {
    _item = read_last_from_indexed_bytesarray(system.user_recasts, _user);
  }

  function read_user_next_recast_from_item(bytes32 _user, bytes32 _current_item)
           public
           constant
           returns (bytes32 _item)
  {
    _item = read_next_from_indexed_bytesarray(system.user_recasts, _user, _current_item);
  }

  function read_user_previous_recast_from_item(bytes32 _user, bytes32 _current_item)
           public
           constant
           returns (bytes32 _item)
  {
    _item = read_previous_from_indexed_bytesarray(system.user_recasts, _user, _current_item);
  }

  function read_user_total_recasts(bytes32 _user)
           public
           constant
           returns (uint256 _total_count)
  {
    _total_count = read_total_indexed_bytesarray(system.user_recasts, _user);
  }

  function create_global_audit(bytes32 _document)
           public
           if_sender_is(CONTRACT_CONTROLLER_ASSETS)
           returns (bool _success)
  {
    _success = system.global_audit_documents.append(_document);
    require(_success);
    system.last_global_audit_time = now;
  }

  /**
  @notice create a new asset item to the `vendor_order` state
  */
  function create_init_item(uint256 _role_id, uint256 _product_id, uint256 _ng_weight, uint256 _effective_ng_weight, address _mint_target)
           public
           if_sender_is(CONTRACT_CONTROLLER_ASSETS)
           returns (bool _success, bytes32 _item)
  {
    (_success, _item) = system.state_machine.create_item(_role_id);
    require(_success);
    system.state_machine.change_item_state(_role_id, _item, STATE_VENDOR_ORDER);
    system.items_by_id[_item].product_id = _product_id;
    system.items_by_id[_item].ng_weight = _ng_weight;
    system.items_by_id[_item].effective_ng_weight = _effective_ng_weight;
    system.items_by_id[_item].mint_target = _mint_target;
  }

  /**
  @notice create a new asset item to be transfered in, set it to `transfer` state
  */
  function create_init_item_transfer(uint256 _role_id, uint256 _product_id, uint256 _ng_weight, uint256 _effective_ng_weight, address _mint_target)
           public
           if_sender_is(CONTRACT_CONTROLLER_ASSETS)
           returns (bool _success, bytes32 _item)
  {
    (_success, _item) = system.state_machine.create_item(_role_id);
    require(_success);
    system.state_machine.change_item_state(_role_id, _item, STATE_TRANSFER);
    system.items_by_id[_item].product_id = _product_id;
    system.items_by_id[_item].ng_weight = _ng_weight;
    system.items_by_id[_item].effective_ng_weight = _effective_ng_weight;
    system.items_by_id[_item].mint_target = _mint_target;
  }

  function internal_check_duplicate_serial_when_fulfilling_order(bytes32 _item, bytes32 _serial)
           internal
  {
    bytes32 _last_asset_with_same_serial = system.last_asset_with_serial[_serial];
    if (_last_asset_with_same_serial != bytes32(0x0)) {
      uint256 _existing_asset_state_id = system.state_machine.get_item_state_id(_last_asset_with_same_serial);
      require(_existing_asset_state_id == STATE_ADMIN_FAILURE
        || _existing_asset_state_id == STATE_AUDIT_FAILURE
        || _existing_asset_state_id == STATE_REDEEMED);
    }
    system.last_asset_with_serial[_serial] = _item;
  }

  /**
  @notice Update an item from either `vendor_order` or `transfer` state to `custodian_delivery` state;
          Save the asset's serial and the related document
  */
  function update_item_fulfill(uint256 _role_id, bytes32 _item, bytes32 _serial, bytes32 _document)
           public
           if_sender_is(CONTRACT_CONTROLLER_ASSETS)
           returns (bool _success, uint256 _from_state, uint256 _new_state)
  {
    (_success, _from_state, _new_state) = system.state_machine.change_item_state(_role_id, _item, STATE_CUSTODIAN_DELIVERY);
    require(_success);
    internal_check_duplicate_serial_when_fulfilling_order(_item, _serial);
    system.asset_documents.append(_item, _document);
    system.items_by_id[_item].serial = _serial;
  }

  /// @notice (custodian) accepts an asset item to the `minted` state
  function update_item_mint(uint256 _role_id, bytes32 _item, bytes32 _document)
           public
           if_sender_is(CONTRACT_CONTROLLER_ASSETS)
           returns (bool _success, address _mint_target, uint256 _effective_ng_weight, uint256 _from_state, uint256 _new_state)
  {
    _mint_target = system.items_by_id[_item].mint_target;
    _effective_ng_weight = system.items_by_id[_item].effective_ng_weight;
    (_success, _from_state, _new_state) = system.state_machine.change_item_state(_role_id, _item, STATE_MINTED);
    require(_success);
    system.items_by_id[_item].time_minted = now;
    system.asset_documents.append(_item, _document);
  }

  /// @notice update a minted asset to the `recasted` state
  function update_item_recast(uint256 _role_id, bytes32 _item, address _redeem_for, bytes32 _document)
           public
           if_sender_is(CONTRACT_CONTROLLER_ASSETS_RECAST)
           returns (bool _success)
  {
    (_success,,) = system.state_machine.change_item_state(_role_id, _item, STATE_RECASTED);
    require(_success);
    system.items_by_id[_item].redeem_for = _redeem_for;
    system.items_by_id[_item].redeem_deadline = now + system.redeem_period * SECONDS_IN_A_DAY;
    system.user_recasts.append(bytes32(_redeem_for), _item);
    system.asset_documents.append(_item, _document);
  }

  /// @notice update a recasted item to the `redeemed` state
  function update_item_redeem(uint256 _role_id, bytes32 _item, bytes32 _document)
           public
           if_sender_is(CONTRACT_CONTROLLER_ASSETS)
           returns (bool _success)
  {
    (_success,,) = system.state_machine.change_item_state(_role_id, _item, STATE_REDEEMED);
    require(_success);
    address _redeem_for = system.items_by_id[_item].redeem_for;
    system.user_recasts.remove_item(bytes32(_redeem_for), _item);
    system.asset_documents.append(_item, _document);
  }

  /// @notice update a recasted item to the `minted` state
  function update_item_remint(uint256 _role_id, bytes32 _item, bytes32 _document)
           public
           if_sender_is(CONTRACT_CONTROLLER_ASSETS)
           returns (bool _success)
  {
    (_success,,) = system.state_machine.change_item_state(_role_id, _item, STATE_MINTED);
    require(_success);
    address _redeem_for = system.items_by_id[_item].redeem_for;
    system.user_recasts.remove_item(bytes32(_redeem_for), _item);
    system.items_by_id[_item].redeem_for = address(0x0); // reset the redeem_for field of this minted asset
    system.asset_documents.append(_item, _document);
  }

  function update_item_to_audit_failure(uint256 _role_id, bytes32 _item, bytes32 _document)
           public
           if_sender_is(CONTRACT_CONTROLLER_ASSETS)
           returns (bool _success, uint256 _from_state, uint256 _new_state)
  {
    (_success, _from_state, _new_state) = system.state_machine.change_item_state(_role_id, _item, STATE_AUDIT_FAILURE);
    require(_success);
    system.asset_documents.append(_item, _document);
  }

  /// @notice (popadmin) create a new asset item to replace an item that failed audit, mark it as `replacement_order`
  function create_init_item_as_replacement(uint256 _role_id, uint256 _product_id, uint256 _ng_weight, uint256 _effective_ng_weight, bytes32 _old_item)
           public
           if_sender_is(CONTRACT_CONTROLLER_ASSETS)
           returns (bool _success, bytes32 _replacement_item)
  {
    (_success, _replacement_item) = system.state_machine.create_item(_role_id);
    require(_success);
    system.state_machine.change_item_state(_role_id, _replacement_item, STATE_REPLACEMENT_ORDER);
    system.items_by_id[_replacement_item].product_id = _product_id;
    system.items_by_id[_replacement_item].ng_weight = _ng_weight;
    system.items_by_id[_replacement_item].effective_ng_weight = _effective_ng_weight;
    system.items_by_id[_replacement_item].replaces = _old_item;
    system.items_by_id[_old_item].replaced_by = _replacement_item;
  }

  /// @notice (vendor) change the state of an asset item from `replacement_order` to `replacement_delivery`
  function update_item_fulfill_replacement(uint256 _role_id, bytes32 _item, bytes32 _serial, bytes32 _document)
           public
           if_sender_is(CONTRACT_CONTROLLER_ASSETS)
           returns (bool _success)
  {
    (_success,,) = system.state_machine.change_item_state(_role_id, _item, STATE_REPLACEMENT_DELIVERY);
    require(_success);
    internal_check_duplicate_serial_when_fulfilling_order(_item, _serial);
    system.asset_documents.append(_item, _document);
    system.items_by_id[_item].serial = _serial;
  }

  /// @notice (custodian) accepts an asset item from `replacement_delivery` state to `minted` state
  function update_item_mint_replacement(uint256 _role_id, bytes32 _item, bytes32 _document)
           public
           if_sender_is(CONTRACT_CONTROLLER_ASSETS)
           returns (bool _success, uint256 _effective_ng_weight)
  {
    _effective_ng_weight = system.items_by_id[_item].effective_ng_weight;
    (_success,,) = system.state_machine.change_item_state(_role_id, _item, STATE_MINTED);
    require(_success);
    system.items_by_id[_item].time_minted = now;
    system.asset_documents.append(_item, _document);
  }

  /// @notice (popadmin) fail an asset that has not been minted
  function update_item_admin_fail(uint256 _role_id, bytes32 _item, bytes32 _document)
           public
           if_sender_is(CONTRACT_CONTROLLER_ASSETS)
           returns (bool _success)
  {
    (_success,,) = system.state_machine.change_item_state(_role_id, _item, STATE_ADMIN_FAILURE);
    require(_success);
    system.asset_documents.append(_item, _document);
  }

  function update_item_add_document(bytes32 _item, bytes32 _document)
           public
           if_sender_is(CONTRACT_CONTROLLER_ASSETS)
           returns (bool _success)
  {
    _success = system.asset_documents.append(_item, _document);
    require(_success);
  }

  function update_redeem_period(uint256 _redeem_period)
           if_sender_is(CONTRACT_CONTROLLER_ASSETS)
           public
           returns (bool _success)
  {
    system.redeem_period = _redeem_period;
    _success = true;
  }
}
function update_item_fulfill(uint256 _role_id, bytes32 _item, bytes32 _serial, bytes32 _document)
         public
         if_sender_is(CONTRACT_CONTROLLER_ASSETS)
         returns (bool _success, uint256 _from_state, uint256 _new_state)
{
  (_success, _from_state, _new_state) = system.state_machine.change_item_state(_role_id, _item, STATE_CUSTODIAN_DELIVERY);
  require(_success);
  internal_check_duplicate_serial_when_fulfilling_order(_item, _serial);
  system.asset_documents.append(_item, _document);
  system.items_by_id[_item].serial = _serial;
}

/**
 @notice Update an item from either `vendor_order` or `transfer` state to `custodian_delivery` state;
 Save the asset's serial and the related document
 */