/**********************************************************************
*  Copyright (c) 2008-2014, Alliance for Sustainable Energy.
*  All rights reserved.
*
*  This library is free software; you can redistribute it and/or
*  modify it under the terms of the GNU Lesser General Public
*  License as published by the Free Software Foundation; either
*  version 2.1 of the License, or (at your option) any later version.
*
*  This library is distributed in the hope that it will be useful,
*  but WITHOUT ANY WARRANTY; without even the implied warranty of
*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
*  Lesser General Public License for more details.
*
*  You should have received a copy of the GNU Lesser General Public
*  License along with this library; if not, write to the Free Software
*  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
**********************************************************************/

#include "UnitFactory.hpp"

#include "ScaleFactory.hpp"
#include "QuantityRegex.hpp"
#include "SIUnit.hpp"
#include "SIUnit_Impl.hpp"
#include "IPUnit.hpp"
#include "IPUnit_Impl.hpp"
#include "BTUUnit.hpp"
#include "BTUUnit_Impl.hpp"
#include "CFMUnit.hpp"
#include "CFMUnit_Impl.hpp"
#include "GPDUnit.hpp"
#include "GPDUnit_Impl.hpp"
#include "MPHUnit.hpp"
#include "MPHUnit_Impl.hpp"
#include "WhUnit.hpp"
#include "WhUnit_Impl.hpp"
#include "ThermUnit.hpp"
#include "ThermUnit_Impl.hpp"
#include "Misc1Unit.hpp"
#include "Misc1Unit_Impl.hpp"
#include "CelsiusUnit.hpp"
#include "CelsiusUnit_Impl.hpp"
#include "FahrenheitUnit.hpp"
#include "FahrenheitUnit_Impl.hpp"

#include "../core/Exception.hpp"
#include "../core/Containers.hpp"
#include "../core/Assert.hpp"

#include <boost/pointer_cast.hpp>

#include <map>
#include <vector>

namespace openstudio{

bool UnitFactorySingleton::registerUnit(CreateUnitCallback createFn,UnitSystem system) {

  Unit thisUnit = createFn();
  std::string standardString = thisUnit.standardString(false);
  std::string prettyString = thisUnit.prettyString(false);

  StandardStringCallbackMap::value_type callbackMapEntry(standardString,createFn);

  // register the callback in main map
  bool result = m_callbackMaps[UnitSystem(UnitSystem::Mixed)].insert(callbackMapEntry).second;
  // already registered--recover if possible
  if (!result) {
    std::stringstream message;
    message << "A callback function for " << standardString << " is already registered"
            << " and there is not enough information to resolve the conflict. "
            << "Note that C and F are assumed to not conflict--all other units use K and R.";
    // mask registry in main map, save in case need to reconstitute
    m_callbackMaps[UnitSystem(UnitSystem::Mixed)][standardString] = 0;

    // make sure string registered elsewhere
    bool found(false);
    for (const auto & callbackMap : m_callbackMaps)
    {
      if ((callbackMap.first == UnitSystem::Mixed) || (callbackMap.first == system)) {
        continue;
      }
      auto lookupPair = callbackMap.second.find(standardString);
      if (lookupPair != callbackMap.second.end()) {
        found = true;
        break;
      }
    }

    if (!found) {
      LOG_AND_THROW(message.str());
    }

  } // if already registered in m_callbackMap

  // register the callback in system-specific map
  result = m_callbackMaps[system].insert(callbackMapEntry).second;
  if (!result && (system != UnitSystem::Mixed)) {
    return result;
  }

  // register standardString as a pseudonym for itself
  registerEquivalentString(standardString,standardString);

  // register prettyString if applicable
  if (prettyString != "") {

    // get prettyString from standardString
    m_prettyStringLookupMap.insert(PrettyStringLookupMap::value_type(standardString,prettyString));

    // get standardString from prettyString
    registerEquivalentString(prettyString,standardString);

    // be able to print things like 1/J
    Unit clonedUnit = thisUnit.clone();
    clonedUnit.setPrettyString("");
    std::string tempPrettyString = boost::regex_replace(prettyString,
                                                        boost::regex("person"),
                                                        "people");
    Unit prettyUnit = parseUnitString(tempPrettyString);
    clonedUnit.pow(-1,1,false); std::string clonedUnitString = clonedUnit.standardString(false);
    prettyUnit.pow(-1,1,false); std::string prettyUnitString = prettyUnit.standardString(false);
    m_prettyStringLookupMap.insert(
          PrettyStringLookupMap::value_type(clonedUnitString,prettyUnitString));
  }

  return result;
}

bool UnitFactorySingleton::registerEquivalentString(const std::string& equivalentString,
                                                    const std::string& standardString) {
  // register the pair
  bool result = m_standardStringLookupMap.insert(
      StandardStringLookupMap::value_type(equivalentString,StringVector(1u,standardString))).second;

  if (!result) {
    // equivalentString is already a key. add this one
    m_standardStringLookupMap[equivalentString].push_back(standardString);
  }

  return result;
}

boost::optional<Unit> UnitFactorySingleton::createUnit(const std::string& unitString,
                                                       UnitSystem system) const
{
  if (m_callbackMaps.size() == 0) {
    LOG(Warn,"UnitFactorySingleton::createUnit called, but the maps appear to be empty.");
  }

  std::string resultCacheKey = unitString + " in unit system " + system.valueName();
  ResultCacheMap::const_iterator findIt = m_resultCacheMap.find(resultCacheKey);
  if (findIt != m_resultCacheMap.end()){
    return findIt->second;
  }

  if (!unitString.empty() && !isUnit(unitString)) {
    LOG(Error,unitString << " is not properly formatted.");
    m_resultCacheMap[resultCacheKey] = boost::none;
    return boost::none;
  }

  OptionalUnit result = createUnitSimple(unitString,system);
  if (result) {
    m_resultCacheMap[resultCacheKey] = result;
    return *result;
  }

  // no luck--start parsing
  std::string wUnitString(unitString);
  ScaleConstant scale = ScaleFactory::instance().createScale(0);

  if (isScaledUnit(wUnitString)) {
    std::pair<std::string,std::string> scaleAndUnit = decomposeScaledUnitString(wUnitString);
    scale = ScaleFactory::instance().createScale(scaleAndUnit.first);
    if (scale().value == 0.0) {
      LOG(Error,"Scaled unit string " << wUnitString << " uses invalid scale abbreviation "
          << scaleAndUnit.first << ".");
      m_resultCacheMap[resultCacheKey] = boost::none;
      return boost::none;
    }
    wUnitString = scaleAndUnit.second;
  }

  // wUnitString should now be compound unit
  std::pair< std::vector<std::string>,std::vector<std::string> > atomicUnits =
      decomposeCompoundUnitString(wUnitString);
  // loop through numerator
  std::vector<std::string>::const_iterator atomicUnitIter;
  std::vector<std::string>::const_iterator vectorEnd = atomicUnits.first.end();
  std::pair<std::string,int> atomicUnit;
  for (atomicUnitIter = atomicUnits.first.begin(); atomicUnitIter != vectorEnd; ++atomicUnitIter) {
    // decompose into baseUnit and exponent
    atomicUnit = decomposeAtomicUnitString(*atomicUnitIter);
    // look for baseUnit
    OptionalUnit baseUnit = createUnitSimple(atomicUnit.first,system);
    if (!baseUnit) {
      // decompose into scale, baseUnit
      std::pair<std::string,std::string> scaleAndBaseUnit = extractScaleAbbreviation(atomicUnit.first);
      if (!scaleAndBaseUnit.first.empty()) {
        baseUnit = createUnitSimple(scaleAndBaseUnit.second,system);
        if (!baseUnit) {
          baseUnit = Unit();
          baseUnit->setBaseUnitExponent(scaleAndBaseUnit.second,1);
        }
        baseUnit->setScale(scaleAndBaseUnit.first);
      }
      else {
        baseUnit = Unit();
        baseUnit->setBaseUnitExponent(atomicUnit.first,1);
      }
    }
    baseUnit->pow(atomicUnit.second);
    if (!result) {
      result = baseUnit;
    }
    else {
      result = (*result) * (*baseUnit);
    }
  }
  // loop through denominator
  vectorEnd = atomicUnits.second.end();
  for (atomicUnitIter = atomicUnits.second.begin(); atomicUnitIter != vectorEnd; ++atomicUnitIter) {
    // decompose into baseUnit and exponent
    atomicUnit = decomposeAtomicUnitString(*atomicUnitIter);
    // look for baseUnit
    OptionalUnit baseUnit = createUnitSimple(atomicUnit.first,system);
    if (!baseUnit) {
      // decompose into scale, baseUnit
      std::pair<std::string,std::string> scaleAndBaseUnit = extractScaleAbbreviation(atomicUnit.first);
      if (!scaleAndBaseUnit.first.empty()) {
        baseUnit = createUnitSimple(scaleAndBaseUnit.second,system);
        if (!baseUnit) {
          LOG(Info,scaleAndBaseUnit.second << " is not a registered baseUnit (in the selected system). "
              << "Returning it as-is in a mixed Unit (not SI, IP, etc.).");
          baseUnit = Unit();
          baseUnit->setBaseUnitExponent(scaleAndBaseUnit.second,1);
        }
        baseUnit->setScale(scaleAndBaseUnit.first);
      }
      else {
        LOG(Info,scaleAndBaseUnit.second << " is not a registered baseUnit (in the selected system). "
              << "Returning it as-is in a mixed Unit (not SI, IP, etc.).");
        baseUnit = Unit();
        baseUnit->setBaseUnitExponent(atomicUnit.first,1);
      }
    }
    baseUnit->pow(atomicUnit.second);
    if (!result) {
      baseUnit->pow(-1);
      result = baseUnit;
    }
    else {
      result = (*result) / (*baseUnit);
    }
  }

  OS_ASSERT(result);

  // impose overall scale
  if (scale().exponent != 0) {
    ScaleOpReturnType resultScale = scale()*result->scale();
    result->setScale(resultScale.first().exponent);
  }

  m_resultCacheMap[resultCacheKey] = result;
  return result;
}

boost::optional<Unit> UnitFactorySingleton::createUnitSimple(const std::string& unitString,
                                                             UnitSystem system) const
{
  if (unitString.empty()) {
    Unit result = createDimensionlessUnit(system);
    if (OptionalTemperatureUnit T = result.optionalCast<TemperatureUnit>()) {
      T->setAsRelative();
    }
    return result;
  }

  OptionalUnit candidate;
  StandardStringLookupMap::const_iterator lookupPair;
  auto standardStringMapEnd = m_standardStringLookupMap.end();
  lookupPair = m_standardStringLookupMap.find(unitString);
  if (lookupPair != standardStringMapEnd) {

    // unitString is registered
    for (const std::string& standardString : lookupPair->second) {

      // instantiate the standardString
      CallbackMapMap::const_iterator callbackMap;
      StandardStringCallbackMap::const_iterator callbackPair;
      OptionalUnit temp;
      // try base map
      callbackMap = m_callbackMaps.find(UnitSystem(UnitSystem::Mixed));
      OS_ASSERT(callbackMap != m_callbackMaps.end());
      callbackPair = callbackMap->second.find(standardString);
      if ((callbackPair != callbackMap->second.end()) && (callbackPair->second != NULL)) {
        temp = callbackPair->second();
      }
      else {
        // try system map
        callbackMap = m_callbackMaps.find(system);
        if (callbackMap != m_callbackMaps.end()) {
          callbackPair = callbackMap->second.find(standardString);
          if ((callbackPair != callbackMap->second.end()) && (callbackPair->second != NULL)) {
            temp = callbackPair->second();
          }
        }

        // try all other maps
        if (!temp) {
          for (int sysValue : UnitSystem::getValues()) {
            UnitSystem tempSystem(sysValue);
            if ((tempSystem == UnitSystem::Mixed) || (tempSystem == system)) {
              continue;
            }
            callbackMap = m_callbackMaps.find(tempSystem);
            if (callbackMap != m_callbackMaps.end()) {
              callbackPair = callbackMap->second.find(standardString);
              if ((callbackPair != callbackMap->second.end()) && (callbackPair->second != NULL)) {
                temp = callbackPair->second();
                break;
              }
            }
          }
        }
      }

      OS_ASSERT(temp);

      // decide whether to keep temp
      if (!candidate || (temp->system() == system)) {
        candidate = temp;
      }

      // decide whether to keep looking
      if ((candidate->system() == system) || (system == UnitSystem::Mixed)) {
        break;
      }

    } // foreach

  } // if

  return candidate;
}

std::string UnitFactorySingleton::lookupPrettyString(const std::string& standardString) const {

  PrettyStringLookupMap::const_iterator lookupPair;
  lookupPair = m_prettyStringLookupMap.find(standardString);
  if (lookupPair == m_prettyStringLookupMap.end()) {
    return "";
  }
  else {
    return lookupPair->second;
  }

}

UnitFactorySingleton::UnitFactorySingleton() {

  // Celsius Base Units ========================================================
  registerUnit(createCelsiusTemperature);

  // Fahrenheit Base Units =====================================================
  registerUnit(createFahrenheitTemperature);

  // SI Base Units =============================================================
  registerUnit(createSIMass);
  registerUnit(createSILength,UnitSystem::SI);
  registerUnit(createSITime,UnitSystem::SI);
  registerUnit(createSITemperature,UnitSystem::SI);
  registerUnit(createSIElectricCurrent,UnitSystem::SI);
  registerUnit(createSILuminousIntensity,UnitSystem::SI);
  registerUnit(createSIAmountOfSubstance,UnitSystem::SI);
  registerUnit(createSIAngle,UnitSystem::SI);
  registerUnit(createSISolidAngle,UnitSystem::SI);
  registerUnit(createSIPeople,UnitSystem::SI);
  registerUnit(createSICycle,UnitSystem::SI);

  // SI Derived Units ==========================================================
  registerUnit(createSIForce);
  registerUnit(createSIEnergy);
  registerUnit(createSIPower);
  registerUnit(createSIElectricCharge,UnitSystem::SI);
  registerUnit(createSIElectricalPotential,UnitSystem::SI);
  registerUnit(createSIElectricCapacitance);
  registerUnit(createSIElectricResistance);
  registerUnit(createSIMagneticFlux);
  registerUnit(createSIMagneticFieldStrength);
  registerUnit(createSIInductance);
  registerUnit(createSILuminousFlux,UnitSystem::SI);
  registerUnit(createSIIlluminance,UnitSystem::SI);
  registerUnit(createSIFrequency,UnitSystem::SI);

  // SI Common Compound Units ==================================================
  registerUnit(createSIEnergyUseIntensity);
  registerUnit(createSIPowerDensity,UnitSystem::SI);
  registerUnit(createSIPowerPerPerson,UnitSystem::SI);
  registerUnit(createSIPressure);
  registerUnit(createSIThermalConductance);
  registerUnit(createSIThermalResistance);
  registerUnit(createSIHeatCapacity);

  // IP Base Units =============================================================
  registerUnit(createIPMass);
  registerUnit(createIPLength,UnitSystem::IP);
  registerUnit(createIPTime,UnitSystem::IP);
  registerUnit(createIPTemperature,UnitSystem::IP);
  registerUnit(createIPElectricCurrent,UnitSystem::IP);
  registerUnit(createIPLuminousIntensity,UnitSystem::IP);
  registerUnit(createIPAmountOfSubstance,UnitSystem::IP);
  registerUnit(createIPAngle,UnitSystem::IP);
  registerUnit(createIPSolidAngle,UnitSystem::IP);
  registerUnit(createIPPeople,UnitSystem::IP);
  registerUnit(createIPCycle,UnitSystem::IP);

  // IP Derived Units ==========================================================
  registerUnit(createIPForce);
  registerUnit(createIPEnergy); // ft*lb-f
  registerUnit(createIPPower); // ft*lb-f/s
  registerUnit(createIPElectricCharge,UnitSystem::IP);
  registerUnit(createIPLuminousFlux,UnitSystem::IP);
  registerUnit(createIPIlluminance,UnitSystem::IP);
  registerUnit(createIPFrequency,UnitSystem::IP);

  // BTU Base Units ============================================================
  registerUnit(createBTUEnergy);
  registerUnit(createBTULength,UnitSystem::BTU);
  registerUnit(createBTUTime,UnitSystem::BTU);
  registerUnit(createBTUTemperature,UnitSystem::BTU);
  registerUnit(createBTUElectricCurrent,UnitSystem::BTU);
  registerUnit(createBTULuminousIntensity,UnitSystem::BTU);
  registerUnit(createBTUAmountOfSubstance,UnitSystem::BTU);
  registerUnit(createBTUAngle,UnitSystem::BTU);
  registerUnit(createBTUSolidAngle,UnitSystem::BTU);
  registerUnit(createBTUPeople,UnitSystem::BTU);
  registerUnit(createBTUCycle,UnitSystem::BTU);

  // BTU Derived Units =========================================================
  registerUnit(createBTUPower);
  registerUnit(createBTULuminousFlux,UnitSystem::BTU);
  registerUnit(createBTUIlluminance,UnitSystem::BTU);

  // CFM Base Units ============================================================
  registerUnit(createCFMLength,UnitSystem::CFM);
  registerUnit(createCFMTime);
  registerUnit(createCFMPower);
  registerUnit(createCFMTemperature,UnitSystem::CFM);
  registerUnit(createCFMElectricCurrent,UnitSystem::CFM);
  registerUnit(createCFMLuminousIntensity,UnitSystem::CFM);
  registerUnit(createCFMAmountOfSubstance,UnitSystem::CFM);
  registerUnit(createCFMAngle,UnitSystem::CFM);
  registerUnit(createCFMSolidAngle,UnitSystem::CFM);
  registerUnit(createCFMPeople,UnitSystem::CFM);
  registerUnit(createCFMCycle,UnitSystem::CFM);

  // CFM Derived Units =========================================================
  registerUnit(createCFMVolumetricFlowrate);
  registerUnit(createCFMLuminousFlux,UnitSystem::CFM);
  registerUnit(createCFMIlluminance,UnitSystem::CFM);
  registerUnit(createCFMFrequency);

  // GPD Base Units ============================================================
  registerUnit(createGPDPressure);
  registerUnit(createGPDLength);
  registerUnit(createGPDTime,UnitSystem::GPD);
  registerUnit(createGPDTemperature,UnitSystem::GPD);
  registerUnit(createGPDElectricCurrent,UnitSystem::GPD);
  registerUnit(createGPDLuminousIntensity,UnitSystem::GPD);
  registerUnit(createGPDAmountOfSubstance,UnitSystem::GPD);
  registerUnit(createGPDAngle,UnitSystem::GPD);
  registerUnit(createGPDSolidAngle,UnitSystem::GPD);
  registerUnit(createGPDPeople,UnitSystem::GPD);
  registerUnit(createGPDCycle,UnitSystem::GPD);

  // GPD Derived Units =========================================================
  registerUnit(createGPDVolume);
  registerUnit(createGPDVolumetricFlowrate);
  registerUnit(createGPDLuminousFlux,UnitSystem::GPD);

  // MPH Base Units ============================================================
  registerUnit(createMPHPressure);
  registerUnit(createMPHLength);
  registerUnit(createMPHTime,UnitSystem::MPH);
  registerUnit(createMPHTemperature,UnitSystem::MPH);
  registerUnit(createMPHElectricCurrent,UnitSystem::MPH);
  registerUnit(createMPHLuminousIntensity,UnitSystem::MPH);
  registerUnit(createMPHAmountOfSubstance,UnitSystem::MPH);
  registerUnit(createMPHAngle,UnitSystem::MPH);
  registerUnit(createMPHSolidAngle,UnitSystem::MPH);
  registerUnit(createMPHPeople,UnitSystem::MPH);
  registerUnit(createMPHCycle,UnitSystem::MPH);

  // MPH Derived Units =========================================================
  registerUnit(createMPHVelocity);
  registerUnit(createMPHLuminousFlux,UnitSystem::MPH);

  // Wh Base Units =============================================================
  registerUnit(createWhPower);
  registerUnit(createWhTime,UnitSystem::Wh);
  registerUnit(createWhLength,UnitSystem::Wh);
  registerUnit(createWhTemperature,UnitSystem::Wh);
  registerUnit(createWhElectricCurrent,UnitSystem::Wh);
  registerUnit(createWhLuminousIntensity,UnitSystem::Wh);
  registerUnit(createWhAmountOfSubstance,UnitSystem::Wh);
  registerUnit(createWhAngle,UnitSystem::Wh);
  registerUnit(createWhSolidAngle,UnitSystem::Wh);
  registerUnit(createWhPeople,UnitSystem::Wh);
  registerUnit(createWhCycle,UnitSystem::Wh);

  // Wh Derived Units ==========================================================
  registerUnit(createWhEnergy);
  registerUnit(createWhElectricalPotential,UnitSystem::Wh);
  registerUnit(createWhLuminousFlux,UnitSystem::Wh);
  registerUnit(createWhIlluminance,UnitSystem::Wh);

  // Therm Base Units ==========================================================
  registerUnit(createThermEnergy);
  registerUnit(createThermLength);
  registerUnit(createThermTime);
  registerUnit(createThermTemperature,UnitSystem::Therm);
  registerUnit(createThermElectricCurrent,UnitSystem::Therm);
  registerUnit(createThermLuminousIntensity,UnitSystem::Therm);
  registerUnit(createThermAmountOfSubstance,UnitSystem::Therm);
  registerUnit(createThermAngle,UnitSystem::Therm);
  registerUnit(createThermSolidAngle,UnitSystem::Therm);
  registerUnit(createThermPeople,UnitSystem::Therm);
  registerUnit(createThermCycle,UnitSystem::Therm);

  // Therm Derived Units =======================================================
  registerUnit(createThermLuminousFlux,UnitSystem::Therm);

  // Misc1 Base Units ==========================================================
  registerUnit(createMisc1Pressure);
  registerUnit(createMisc1Length);
  registerUnit(createMisc1Time,UnitSystem::Misc1);
  registerUnit(createMisc1Temperature,UnitSystem::Misc1);
  registerUnit(createMisc1ElectricCurrent,UnitSystem::Misc1);
  registerUnit(createMisc1LuminousIntensity,UnitSystem::Misc1);
  registerUnit(createMisc1AmountOfSubstance,UnitSystem::Misc1);
  registerUnit(createMisc1Angle,UnitSystem::Misc1);
  registerUnit(createMisc1SolidAngle,UnitSystem::Misc1);
  registerUnit(createMisc1People,UnitSystem::Misc1);
  registerUnit(createMisc1Cycle,UnitSystem::Misc1);

  // Misc1 Derived Units =======================================================
  registerUnit(createMisc1Volume);
  registerUnit(createMisc1LuminousFlux,UnitSystem::Misc1);

  // Mixed Derived Units =======================================================
  registerUnit(createIPPowerDensity);
  registerUnit(createIPPressure);

  // Equivalent Strings ========================================================
  registerEquivalentString("person","people");
  registerEquivalentString("cycles","cycle");
  registerEquivalentString("lb","lb_m");
  registerEquivalentString("CFM","ft^3/min");
  registerEquivalentString("Therm","therm");
  registerEquivalentString("thm","therm");
  registerEquivalentString("mile","mi");
  registerEquivalentString("miles","mi");
  registerEquivalentString("days","day");
  registerEquivalentString("year","yr");
  registerEquivalentString("years","yr");
  registerEquivalentString("hr","h");
  registerEquivalentString("hrs","h");
}

UnitSystem getSystem(const std::string& unitString) {

  OptionalUnit unit;

  // pick up preferred unit system for strings like "C" and "F"
  unit = createUnit(unitString,UnitSystem::Mixed);
  if (unit && (unit->system() != UnitSystem::Mixed)) {
    return unit->system();
  }

  unit = createUnit(unitString,UnitSystem::SI);
  if (unit && (unit->system() != UnitSystem::Mixed)) {
    return unit->system();
  }

  unit = createUnit(unitString,UnitSystem::IP);
  if (unit && (unit->system() != UnitSystem::Mixed)) {
    return unit->system();
  }

  unit = createUnit(unitString,UnitSystem::BTU);
  if (unit && (unit->system() != UnitSystem::Mixed)) {
    return unit->system();
  }

  unit = createUnit(unitString,UnitSystem::CFM);
  if (unit && (unit->system() != UnitSystem::Mixed)) {
    return unit->system();
  }

  unit = createUnit(unitString,UnitSystem::GPD);
  if (unit && (unit->system() != UnitSystem::Mixed)) {
    return unit->system();
  }

  unit = createUnit(unitString,UnitSystem::MPH);
  if (unit && (unit->system() != UnitSystem::Mixed)) {
    return unit->system();
  }

  unit = createUnit(unitString,UnitSystem::Wh);
  if (unit && (unit->system() != UnitSystem::Mixed)) {
    return unit->system();
  }

  unit = createUnit(unitString,UnitSystem::Therm);
  if (unit && (unit->system() != UnitSystem::Mixed)) {
    return unit->system();
  }

  unit = createUnit(unitString,UnitSystem::Misc1);
  if (unit && (unit->system() != UnitSystem::Mixed)) {
    return unit->system();
  }

  unit = createUnit(unitString,UnitSystem::Celsius);
  if (unit && (unit->system() != UnitSystem::Mixed)) {
    return unit->system();
  }

  unit = createUnit(unitString,UnitSystem::Fahrenheit);
  if (unit && (unit->system() != UnitSystem::Mixed)) {
    return unit->system();
  }

  return UnitSystem::Mixed;
}

bool isInSystem(const std::string& unitString, UnitSystem system) {
  OptionalUnit unit = createUnit(unitString,system);
  if (unit && ((unit->system() == system) || (system == UnitSystem::Mixed))) {
    return true;
  }
  return false;
}

bool containsRegisteredBaseUnit(const std::string& unitString) {
  if (isUnitString(unitString)) {
    Unit u = createUnit(unitString).get();
    StringVector bus = u.baseUnits();
    for (const std::string& bu : bus) {
      if (getSystem(bu) != UnitSystem::Mixed) {
        return true;
      }
    }
  }
  return false;
}

std::string extractUnitString(const std::string& text) {
  boost::smatch m;
  std::string wtext(text);
  std::string result;

  // first handle people/100 m^2
  if (boost::regex_search(wtext,m,regexEmbeddedDirectScaledUnit())) {
    // main match at 1, 5, 9, or 13
    for (unsigned i = 1; i < 14; i += 4) {
      result = std::string(m[i].first,m[i].second);
      if (!result.empty()) {
        return result;
      }
    }
  }

  std::string::const_iterator wbegin = wtext.begin();
  std::string::const_iterator wend = wtext.end();

  while (boost::regex_search(wbegin, wend, m,regexEmbeddedUnit())) {
    unsigned i = 1;
    result = std::string(m[i].first,m[i].second);
    while (result.empty()) {
      ++i;
      if (i < m.size()) { result = std::string(m[i].first,m[i].second); }
      else { break; }
    }
    if (containsRegisteredBaseUnit(result)) { break; }
    result.clear();
    wbegin = m[0].second;
  }

  return result;
}

std::string convertToStandardForm(const std::string& unitString) {
  std::string result(unitString);
  if (isDirectScaledUnit(unitString)) {
    std::pair<std::string,std::pair<unsigned,std::string> > fragments =
        decomposeDirectScaledUnit(unitString);
    ScaleConstant s;
    s = ScaleFactory::instance().createScale(-static_cast<int>(fragments.second.first));
    if (s().value != 0.0) {
      std::stringstream ss;
      ss << s().abbr << "(";
      if (!fragments.second.second.empty()) {
        ss << fragments.first << fragments.second.second;
      }
      else {
        fragments.first.resize(fragments.first.size() - 1); // remove '/'
        ss << fragments.first;
      }
      ss << ")";
      result = ss.str();
    }
  }
  return result;
}

std::string replaceUnitString(const std::string& text,const std::string& newUnitString) {
  std::string toReplace = extractUnitString(text);
  std::string result(text);
  if (!toReplace.empty()) {
    result = boost::regex_replace(text,boost::regex(toReplace,boost::regex_constants::literal),
                                  newUnitString,boost::regex_constants::format_literal);
  }
  return result;
}

bool isUnitString(const std::string& unitString) {
  OptionalUnit unit = createUnit(unitString);
  return unit;
}

Unit createDimensionlessUnit(UnitSystem system) {
  switch (system.value()) {
  case UnitSystem::Mixed:
    return Unit();
  case UnitSystem::SI:
    return SIUnit();
  case UnitSystem::IP:
    return IPUnit();
  case UnitSystem::BTU:
    return BTUUnit();
  case UnitSystem::CFM:
    return CFMUnit();
  case UnitSystem::GPD:
    return GPDUnit();
  case UnitSystem::MPH:
    return MPHUnit();
  case UnitSystem::Therm:
    return ThermUnit();
  case UnitSystem::Wh:
    return WhUnit();
  case UnitSystem::Misc1:
    return Misc1Unit();
  case UnitSystem::Celsius:
    return CelsiusUnit();
  case UnitSystem::Fahrenheit:
    return FahrenheitUnit();
  default:
    OS_ASSERT(false);
  }
  return Unit();
}

boost::optional<Unit> createUnit(const std::string& unitString,UnitSystem system) {
  return UnitFactory::instance().createUnit(unitString,system);
}

boost::optional<Unit> createUnit(const std::string& unitString) {
  OptionalUnit unit;

  // pick up preferred unit system for strings like "C" and "F"
  unit = createUnit(unitString,UnitSystem::Mixed);
  if (unit && (unit->system() != UnitSystem::Mixed)) {
    return unit;
  }

  unit = createUnit(unitString,UnitSystem::SI);
  if (unit && (unit->system() != UnitSystem::Mixed)) {
    return unit;
  }

  unit = createUnit(unitString,UnitSystem::IP);
  if (unit && (unit->system() != UnitSystem::Mixed)) {
    return unit;
  }

  unit = createUnit(unitString,UnitSystem::BTU);
  if (unit && (unit->system() != UnitSystem::Mixed)) {
    return unit;
  }

  unit = createUnit(unitString,UnitSystem::CFM);
  if (unit && (unit->system() != UnitSystem::Mixed)) {
    return unit;
  }

  unit = createUnit(unitString,UnitSystem::GPD);
  if (unit && (unit->system() != UnitSystem::Mixed)) {
    return unit;
  }

  unit = createUnit(unitString,UnitSystem::MPH);
  if (unit && (unit->system() != UnitSystem::Mixed)) {
    return unit;
  }

  unit = createUnit(unitString,UnitSystem::Wh);
  if (unit && (unit->system() != UnitSystem::Mixed)) {
    return unit;
  }

  unit = createUnit(unitString,UnitSystem::Therm);
  if (unit && (unit->system() != UnitSystem::Mixed)) {
    return unit;
  }

  unit = createUnit(unitString,UnitSystem::Misc1);
  if (unit && (unit->system() != UnitSystem::Mixed)) {
    return unit;
  }

  unit = createUnit(unitString,UnitSystem::Celsius);
  if (unit && (unit->system() != UnitSystem::Mixed)) {
    return unit;
  }

  unit = createUnit(unitString,UnitSystem::Fahrenheit);
  if (unit && (unit->system() != UnitSystem::Mixed)) {
    return unit;
  }

  return createUnit(unitString,UnitSystem::Mixed);
}

bool unitStringsEqual(const std::string& uStr1,const std::string& uStr2) {
  if (isUnitString(uStr1) && isUnitString(uStr2)) {
    Unit u1 = createUnit(uStr1).get();
    Unit u2 = createUnit(uStr2).get();
    return (u1 == u2);
  }
  return false;
}

Unit createIPPowerDensity() {
  Unit result;
  result.setBaseUnitExponent("kg",1);
  result.setBaseUnitExponent("m",2);
  result.setBaseUnitExponent("s",-3);
  result.setBaseUnitExponent("ft",-2);
  result.setPrettyString("W/ft^2");
  return result;
}

Unit createGPMVolumetricFlowrate() {
  Unit result;
  result.setBaseUnitExponent("crgal",3);
  result.setBaseUnitExponent("min",-1);
  result.setPrettyString("gal/min");
  return result;
}

Unit createIPPressure() {
  Unit result;
  result.setBaseUnitExponent("lb_f",1);
  result.setBaseUnitExponent("in",-2);
  result.setPrettyString("psi");
  return result;
}

} // openstudio


// Copyright (c) 2017-2020, The Monero Project
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
//    conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
//    of conditions and the following disclaimer in the documentation and/or other
//    materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
//    used to endorse or promote products derived from this software without specific
//    prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//

#include "version.h"
#include "protocol.hpp"
#include <unordered_map>
#include <set>
#include <utility>
#include <boost/endian/conversion.hpp>
#include <common/apply_permutation.h>
#include <common/json_util.h>
#include <crypto/hmac-keccak.h>
#include <ringct/rctSigs.h>
#include <ringct/bulletproofs.h>
#include "cryptonote_config.h"
#include <sodium.h>
#include <sodium/crypto_verify_32.h>
#include <sodium/crypto_aead_chacha20poly1305.h>

#define GET_FIELD_STRING(name, type, jtype) field_##name = std::string(json[#name].GetString(), json[#name].GetStringLength())
#define GET_FIELD_OTHER(name, type, jtype) field_##name = static_cast<type>(json[#name].Get##jtype())

#define GET_STRING_FROM_JSON(json, name, type, mandatory, def) \
  GET_FIELD_FROM_JSON_EX(json, name, type, String, mandatory, def, GET_FIELD_STRING)

#define GET_FIELD_FROM_JSON(json, name, type, jtype, mandatory, def) \
  GET_FIELD_FROM_JSON_EX(json, name, type, jtype, mandatory, def, GET_FIELD_OTHER)

#define GET_FIELD_FROM_JSON_EX(json, name, type, jtype, mandatory, def, VAL) \
  type field_##name = static_cast<type>(def);                        \
  bool field_##name##_found = false;                                 \
  (void)field_##name##_found;                                        \
  do if (json.HasMember(#name))                                      \
  {                                                                  \
    if (json[#name].Is##jtype())                                     \
    {                                                                \
      VAL(name, type, jtype);                                        \
      field_##name##_found = true;                                   \
    }                                                                \
    else                                                             \
    {                                                                \
      throw std::invalid_argument("Field " #name " found in JSON, but not " #jtype); \
    }                                                                \
  }                                                                  \
  else if (mandatory)                                                \
  {                                                                  \
    throw std::invalid_argument("Field " #name " not found in JSON");\
  } while(0)

namespace hw{
namespace trezor{
namespace protocol{

  std::string key_to_string(const ::crypto::ec_point & key){
    return std::string(key.data, sizeof(key.data));
  }

  std::string key_to_string(const ::crypto::ec_scalar & key){
    return std::string(key.data, sizeof(key.data));
  }

  std::string key_to_string(const ::crypto::hash & key){
    return std::string(key.data, sizeof(key.data));
  }

  std::string key_to_string(const ::rct::key & key){
    return std::string(reinterpret_cast<const char*>(key.bytes), sizeof(key.bytes));
  }

  void string_to_key(::crypto::ec_scalar & key, const std::string & str){
    if (str.size() != sizeof(key.data)){
      throw std::invalid_argument(std::string("Key has to have ") + std::to_string(sizeof(key.data)) + " B");
    }
    memcpy(key.data, str.data(), sizeof(key.data));
  }

  void string_to_key(::crypto::ec_point & key, const std::string & str){
    if (str.size() != sizeof(key.data)){
      throw std::invalid_argument(std::string("Key has to have ") + std::to_string(sizeof(key.data)) + " B");
    }
    memcpy(key.data, str.data(), sizeof(key.data));
  }

  void string_to_key(::rct::key & key, const std::string & str){
    if (str.size() != sizeof(key.bytes)){
      throw std::invalid_argument(std::string("Key has to have ") + std::to_string(sizeof(key.bytes)) + " B");
    }
    memcpy(key.bytes, str.data(), sizeof(key.bytes));
  }

namespace crypto {
namespace chacha {

  void decrypt(const void* ciphertext, size_t length, const uint8_t* key, const uint8_t* iv, char* plaintext, size_t *plaintext_len){
    CHECK_AND_ASSERT_THROW_MES(length >= TAG_SIZE, "Ciphertext length too small");
    CHECK_AND_ASSERT_THROW_MES(!plaintext_len || *plaintext_len >= (length - TAG_SIZE), "Plaintext length too small");

    unsigned long long int res_len = plaintext_len ? *plaintext_len : length;
    auto r = crypto_aead_chacha20poly1305_ietf_decrypt(
        reinterpret_cast<unsigned char *>(plaintext), &res_len, nullptr,
        static_cast<const unsigned char *>(ciphertext), length, nullptr, 0, iv, key);

    if (r != 0){
      throw exc::Poly1305TagInvalid();
    }

    if (plaintext_len){
      *plaintext_len = (size_t) res_len;
    }
  }

}
}

// Cold Key image sync
namespace ki {

  bool key_image_data(wallet_shim * wallet,
                      const std::vector<tools::wallet2::transfer_details> & transfers,
                      std::vector<MoneroTransferDetails> & res)
  {
    for(auto & td : transfers){
      ::crypto::public_key tx_pub_key = wallet->get_tx_pub_key_from_received_outs(td);
      const std::vector<::crypto::public_key> additional_tx_pub_keys = cryptonote::get_additional_tx_pub_keys_from_extra(td.m_tx);

      res.emplace_back();
      auto & cres = res.back();

      cres.set_out_key(key_to_string(boost::get<cryptonote::txout_to_key>(td.m_tx.vout[td.m_internal_output_index].target).key));
      cres.set_tx_pub_key(key_to_string(tx_pub_key));
      cres.set_internal_output_index(td.m_internal_output_index);
      for(auto & aux : additional_tx_pub_keys){
        cres.add_additional_tx_pub_keys(key_to_string(aux));
      }
    }

    return true;
  }

  std::string compute_hash(const MoneroTransferDetails & rr){
    KECCAK_CTX kck;
    uint8_t md[32];

    CHECK_AND_ASSERT_THROW_MES(rr.out_key().size() == 32, "Invalid out_key size");
    CHECK_AND_ASSERT_THROW_MES(rr.tx_pub_key().size() == 32, "Invalid tx_pub_key size");

    keccak_init(&kck);
    keccak_update(&kck, reinterpret_cast<const uint8_t *>(rr.out_key().data()), 32);
    keccak_update(&kck, reinterpret_cast<const uint8_t *>(rr.tx_pub_key().data()), 32);
    for (const auto &aux : rr.additional_tx_pub_keys()){
      CHECK_AND_ASSERT_THROW_MES(aux.size() == 32, "Invalid aux size");
      keccak_update(&kck, reinterpret_cast<const uint8_t *>(aux.data()), 32);
    }

    auto index_serialized = tools::get_varint_data(rr.internal_output_index());
    keccak_update(&kck, reinterpret_cast<const uint8_t *>(index_serialized.data()), index_serialized.size());
    keccak_finish(&kck, md);
    return std::string(reinterpret_cast<const char*>(md), sizeof(md));
  }

  void generate_commitment(std::vector<MoneroTransferDetails> & mtds,
                           const std::vector<tools::wallet2::transfer_details> & transfers,
                           std::shared_ptr<messages::monero::MoneroKeyImageExportInitRequest> & req)
  {
    req = std::make_shared<messages::monero::MoneroKeyImageExportInitRequest>();

    KECCAK_CTX kck;
    uint8_t final_hash[32];
    keccak_init(&kck);

    for(auto &cur : mtds){
      auto hash = compute_hash(cur);
      keccak_update(&kck, reinterpret_cast<const uint8_t *>(hash.data()), hash.size());
    }
    keccak_finish(&kck, final_hash);

    req = std::make_shared<messages::monero::MoneroKeyImageExportInitRequest>();
    req->set_hash(std::string(reinterpret_cast<const char*>(final_hash), 32));
    req->set_num(transfers.size());

    std::unordered_map<uint32_t, std::set<uint32_t>> sub_indices;
    for (auto &cur : transfers){
      auto search = sub_indices.emplace(cur.m_subaddr_index.major, std::set<uint32_t>());
      auto & st = search.first->second;
      st.insert(cur.m_subaddr_index.minor);
    }

    for (auto& x: sub_indices){
      auto subs = req->add_subs();
      subs->set_account(x.first);
      for(auto minor : x.second){
        subs->add_minor_indices(minor);
      }
    }
  }

  void live_refresh_ack(const ::crypto::secret_key & view_key_priv,
                        const ::crypto::public_key& out_key,
                        const std::shared_ptr<messages::monero::MoneroLiveRefreshStepAck> & ack,
                        ::cryptonote::keypair& in_ephemeral,
                        ::crypto::key_image& ki)
  {
    std::string str_out_key(out_key.data, sizeof(out_key.data));
    auto enc_key = protocol::tx::compute_enc_key(view_key_priv, str_out_key, ack->salt());

    const size_t len_ciphertext = ack->key_image().size();  // IV || keys
    CHECK_AND_ASSERT_THROW_MES(len_ciphertext > crypto::chacha::IV_SIZE + crypto::chacha::TAG_SIZE, "Invalid size");

    size_t ki_len = len_ciphertext - crypto::chacha::IV_SIZE - crypto::chacha::TAG_SIZE;
    std::unique_ptr<uint8_t[]> plaintext(new uint8_t[ki_len]);
    uint8_t * buff = plaintext.get();

    protocol::crypto::chacha::decrypt(
        ack->key_image().data() + crypto::chacha::IV_SIZE,
        len_ciphertext - crypto::chacha::IV_SIZE,
        reinterpret_cast<const uint8_t *>(enc_key.data),
        reinterpret_cast<const uint8_t *>(ack->key_image().data()),
        reinterpret_cast<char *>(buff), &ki_len);

    CHECK_AND_ASSERT_THROW_MES(ki_len == 3*32, "Invalid size");
    ::crypto::signature sig{};
    memcpy(ki.data, buff, 32);
    memcpy(sig.c.data, buff + 32, 32);
    memcpy(sig.r.data, buff + 64, 32);
    in_ephemeral.pub = out_key;
    in_ephemeral.sec = ::crypto::null_skey;

    // Verification
    std::vector<const ::crypto::public_key*> pkeys;
    pkeys.push_back(&out_key);

    CHECK_AND_ASSERT_THROW_MES(rct::scalarmultKey(rct::ki2rct(ki), rct::curveOrder()) == rct::identity(),
                               "Key image out of validity domain: key image " << epee::string_tools::pod_to_hex(ki));

    CHECK_AND_ASSERT_THROW_MES(::crypto::check_ring_signature((const ::crypto::hash&)ki, ki, pkeys, &sig),
                               "Signature failed for key image " << epee::string_tools::pod_to_hex(ki)
                                                                 << ", signature " + epee::string_tools::pod_to_hex(sig)
                                                                 << ", pubkey " + epee::string_tools::pod_to_hex(*pkeys[0]));
  }
}

// Cold transaction signing
namespace tx {

  void translate_address(MoneroAccountPublicAddress * dst, const cryptonote::account_public_address * src){
    dst->set_view_public_key(key_to_string(src->m_view_public_key));
    dst->set_spend_public_key(key_to_string(src->m_spend_public_key));
  }

  void translate_dst_entry(MoneroTransactionDestinationEntry * dst, const cryptonote::tx_destination_entry * src){
    dst->set_amount(src->amount);
    dst->set_is_subaddress(src->is_subaddress);
    dst->set_is_integrated(src->is_integrated);
    dst->set_original(src->original);
    translate_address(dst->mutable_addr(), &(src->addr));
  }

  void translate_src_entry(MoneroTransactionSourceEntry * dst, const cryptonote::tx_source_entry * src){
    for(auto & cur : src->outputs){
      auto out = dst->add_outputs();
      out->set_idx(cur.first);
      translate_rct_key(out->mutable_key(), &(cur.second));
    }

    dst->set_real_output(src->real_output);
    dst->set_real_out_tx_key(key_to_string(src->real_out_tx_key));
    for(auto & cur : src->real_out_additional_tx_keys){
      dst->add_real_out_additional_tx_keys(key_to_string(cur));
    }

    dst->set_real_output_in_tx_index(src->real_output_in_tx_index);
    dst->set_amount(src->amount);
    dst->set_rct(src->rct);
    dst->set_mask(key_to_string(src->mask));
    translate_klrki(dst->mutable_multisig_klrki(), &(src->multisig_kLRki));
  }

  void translate_klrki(MoneroMultisigKLRki * dst, const rct::multisig_kLRki * src){
    dst->set_k(key_to_string(src->k));
    dst->set_l(key_to_string(src->L));
    dst->set_r(key_to_string(src->R));
    dst->set_ki(key_to_string(src->ki));
  }

  void translate_rct_key(MoneroRctKey * dst, const rct::ctkey * src){
    dst->set_dest(key_to_string(src->dest));
    dst->set_commitment(key_to_string(src->mask));
  }

  std::string hash_addr(const MoneroAccountPublicAddress * addr, std::optional<uint64_t> amount, std::optional<bool> is_subaddr){
    return hash_addr(addr->spend_public_key(), addr->view_public_key(), amount, is_subaddr);
  }

  std::string hash_addr(const std::string & spend_key, const std::string & view_key, std::optional<uint64_t> amount, std::optional<bool> is_subaddr){
    ::crypto::public_key spend{}, view{};
    if (spend_key.size() != 32 || view_key.size() != 32){
      throw std::invalid_argument("Public keys have invalid sizes");
    }

    memcpy(spend.data, spend_key.data(), 32);
    memcpy(view.data, view_key.data(), 32);
    return hash_addr(&spend, &view, amount, is_subaddr);
  }

  std::string hash_addr(const ::crypto::public_key * spend_key, const ::crypto::public_key * view_key, std::optional<uint64_t> amount, std::optional<bool> is_subaddr){
    char buff[64+8+1];
    size_t offset = 0;

    memcpy(buff + offset, spend_key->data, 32); offset += 32;
    memcpy(buff + offset, view_key->data, 32); offset += 32;

    if (amount){
      memcpy(buff + offset, (uint8_t*) &(amount.get()), sizeof(amount.get())); offset += sizeof(amount.get());
    }

    if (is_subaddr){
      buff[offset] = is_subaddr.get();
      offset += 1;
    }

    return std::string(buff, offset);
  }

  ::crypto::secret_key compute_enc_key(const ::crypto::secret_key & private_view_key, const std::string & aux, const std::string & salt)
  {
    uint8_t hash[32];
    KECCAK_CTX ctx;
    ::crypto::secret_key res;

    keccak_init(&ctx);
    keccak_update(&ctx, (const uint8_t *) private_view_key.data, sizeof(private_view_key.data));
    if (!aux.empty()){
      keccak_update(&ctx, (const uint8_t *) aux.data(), aux.size());
    }
    keccak_finish(&ctx, hash);
    keccak(hash, sizeof(hash), hash, sizeof(hash));

    hmac_keccak_hash(hash, (const uint8_t *) salt.data(), salt.size(), hash, sizeof(hash));
    memcpy(res.data, hash, sizeof(hash));
    memwipe(hash, sizeof(hash));
    return res;
  }

  TData::TData() {
    rsig_type = 0;
    bp_version = 0;
    cur_input_idx = 0;
    cur_output_idx = 0;
    cur_batch_idx = 0;
    cur_output_in_batch_idx = 0;
  }

  Signer::Signer(wallet_shim *wallet2, const unsigned_tx_set * unsigned_tx, size_t tx_idx, hw::tx_aux_data * aux_data) {
    m_wallet2 = wallet2;
    m_unsigned_tx = unsigned_tx;
    m_aux_data = aux_data;
    m_tx_idx = tx_idx;
    m_ct.tx_data = cur_tx();
    m_multisig = false;
    m_client_version = 1;
  }

  void Signer::extract_payment_id(){
    const std::vector<uint8_t>& tx_extra = cur_tx().extra;
    m_ct.tsx_data.set_payment_id("");

    std::vector<cryptonote::tx_extra_field> tx_extra_fields;
    cryptonote::parse_tx_extra(tx_extra, tx_extra_fields); // ok if partially parsed
    cryptonote::tx_extra_nonce extra_nonce;

    ::crypto::hash payment_id{};
    if (find_tx_extra_field_by_type(tx_extra_fields, extra_nonce))
    {
      ::crypto::hash8 payment_id8{};
      if(cryptonote::get_encrypted_payment_id_from_tx_extra_nonce(extra_nonce.nonce, payment_id8))
      {
        m_ct.tsx_data.set_payment_id(std::string(payment_id8.data, 8));
      }
      else if (cryptonote::get_payment_id_from_tx_extra_nonce(extra_nonce.nonce, payment_id))
      {
        m_ct.tsx_data.set_payment_id(std::string(payment_id.data, 32));
      }
    }
  }

  static unsigned get_rsig_type(const rct::RCTConfig &rct_config, size_t num_outputs){
    if (rct_config.range_proof_type == rct::RangeProofBorromean){
      return rct::RangeProofBorromean;
    } else if (num_outputs > BULLETPROOF_MAX_OUTPUTS){
      return rct::RangeProofMultiOutputBulletproof;
    } else {
      return rct::RangeProofPaddedBulletproof;
    }
  }

  static void generate_rsig_batch_sizes(std::vector<uint64_t> &batches, unsigned rsig_type, size_t num_outputs){
    size_t amount_batched = 0;

    while(amount_batched < num_outputs){
      if (rsig_type == rct::RangeProofBorromean || rsig_type == rct::RangeProofBulletproof) {
        batches.push_back(1);
        amount_batched += 1;

      } else if (rsig_type == rct::RangeProofPaddedBulletproof){
        if (num_outputs > BULLETPROOF_MAX_OUTPUTS){
          throw std::invalid_argument("BP padded can support only BULLETPROOF_MAX_OUTPUTS statements");
        }
        batches.push_back(num_outputs);
        amount_batched += num_outputs;

      } else if (rsig_type == rct::RangeProofMultiOutputBulletproof){
        size_t batch_size = 1;
        while (batch_size * 2 + amount_batched <= num_outputs && batch_size * 2 <= BULLETPROOF_MAX_OUTPUTS){
          batch_size *= 2;
        }
        batch_size = std::min(batch_size, num_outputs - amount_batched);
        batches.push_back(batch_size);
        amount_batched += batch_size;

      } else {
        throw std::invalid_argument("Unknown rsig type");
      }
    }
  }

  void Signer::compute_integrated_indices(TsxData * tsx_data){
    if (m_aux_data == nullptr || m_aux_data->tx_recipients.empty()){
      return;
    }

    auto & chg = tsx_data->change_dts();
    std::string change_hash = hash_addr(&chg.addr(), chg.amount(), chg.is_subaddress());

    std::vector<uint32_t> integrated_indices;
    std::set<std::string> integrated_hashes;
    for (auto & cur : m_aux_data->tx_recipients){
      if (!cur.has_payment_id){
        continue;
      }
      integrated_hashes.emplace(hash_addr(&cur.address.m_spend_public_key, &cur.address.m_view_public_key));
    }

    ssize_t idx = -1;
    for (auto & cur : tsx_data->outputs()){
      idx += 1;

      std::string c_hash = hash_addr(&cur.addr(), cur.amount(), cur.is_subaddress());
      if (c_hash == change_hash || cur.is_subaddress()){
        continue;
      }

      c_hash = hash_addr(&cur.addr());
      if (integrated_hashes.find(c_hash) != integrated_hashes.end()){
        integrated_indices.push_back((uint32_t)idx);
      }
    }

    if (!integrated_indices.empty()){
      assign_to_repeatable(tsx_data->mutable_integrated_indices(), integrated_indices.begin(), integrated_indices.end());
    }
  }

  std::shared_ptr<messages::monero::MoneroTransactionInitRequest> Signer::step_init(){
    // extract payment ID from construction data
    auto & tsx_data = m_ct.tsx_data;
    auto & tx = cur_tx();

    m_ct.tx.version = 2;
    m_ct.tx.unlock_time = tx.unlock_time;
    m_client_version = (m_aux_data->client_version ? m_aux_data->client_version.get() : 1);

    tsx_data.set_version(1);
    tsx_data.set_client_version(client_version());
    tsx_data.set_unlock_time(tx.unlock_time);
    tsx_data.set_num_inputs(static_cast<google::protobuf::uint32>(tx.sources.size()));
    tsx_data.set_mixin(static_cast<google::protobuf::uint32>(tx.sources[0].outputs.size() - 1));
    tsx_data.set_account(tx.subaddr_account);
    tsx_data.set_monero_version(std::string(SUMOKOIN_VERSION) + "|" + SUMOKOIN_VERSION_TAG);
    tsx_data.set_hard_fork(m_aux_data->hard_fork ? m_aux_data->hard_fork.get() : 0);
    assign_to_repeatable(tsx_data.mutable_minor_indices(), tx.subaddr_indices.begin(), tx.subaddr_indices.end());

    // Rsig decision
    auto rsig_data = tsx_data.mutable_rsig_data();
    m_ct.rsig_type = get_rsig_type(tx.rct_config, tx.splitted_dsts.size());
    rsig_data->set_rsig_type(m_ct.rsig_type);
    if (tx.rct_config.range_proof_type != rct::RangeProofBorromean){
      m_ct.bp_version = (m_aux_data->bp_version ? m_aux_data->bp_version.get() : 1);
      rsig_data->set_bp_version((uint32_t) m_ct.bp_version);
    }

    generate_rsig_batch_sizes(m_ct.grouping_vct, m_ct.rsig_type, tx.splitted_dsts.size());
    assign_to_repeatable(rsig_data->mutable_grouping(), m_ct.grouping_vct.begin(), m_ct.grouping_vct.end());

    translate_dst_entry(tsx_data.mutable_change_dts(), &(tx.change_dts));
    for(auto & cur : tx.splitted_dsts){
      auto dst = tsx_data.mutable_outputs()->Add();
      translate_dst_entry(dst, &cur);
    }

    compute_integrated_indices(&tsx_data);

    int64_t fee = 0;
    for(auto & cur_in : tx.sources){
      fee += cur_in.amount;
    }
    for(auto & cur_out : tx.splitted_dsts){
      fee -= cur_out.amount;
    }
    if (fee < 0){
      throw std::invalid_argument("Fee cannot be negative");
    }

    tsx_data.set_fee(static_cast<google::protobuf::uint64>(fee));
    this->extract_payment_id();

    auto init_req = std::make_shared<messages::monero::MoneroTransactionInitRequest>();
    init_req->set_version(0);
    init_req->mutable_tsx_data()->CopyFrom(tsx_data);
    return init_req;
  }

  void Signer::step_init_ack(std::shared_ptr<const messages::monero::MoneroTransactionInitAck> ack){
    if (ack->has_rsig_data()){
      m_ct.rsig_param = std::make_shared<MoneroRsigData>(ack->rsig_data());
    }

    assign_from_repeatable(&(m_ct.tx_out_entr_hmacs), ack->hmacs().begin(), ack->hmacs().end());
  }

  std::shared_ptr<messages::monero::MoneroTransactionSetInputRequest> Signer::step_set_input(size_t idx){
    CHECK_AND_ASSERT_THROW_MES(idx < cur_tx().sources.size(), "Invalid source index");
    m_ct.cur_input_idx = idx;
    auto res = std::make_shared<messages::monero::MoneroTransactionSetInputRequest>();
    translate_src_entry(res->mutable_src_entr(), &(cur_tx().sources[idx]));
    return res;
  }

  void Signer::step_set_input_ack(std::shared_ptr<const messages::monero::MoneroTransactionSetInputAck> ack){
    auto & vini_str = ack->vini();

    cryptonote::txin_v vini;
    if (!cn_deserialize(vini_str.data(), vini_str.size(), vini)){
      throw exc::ProtocolException("Cannot deserialize vin[i]");
    }

    m_ct.tx.vin.emplace_back(vini);
    m_ct.tx_in_hmacs.push_back(ack->vini_hmac());
    m_ct.pseudo_outs.push_back(ack->pseudo_out());
    m_ct.pseudo_outs_hmac.push_back(ack->pseudo_out_hmac());
    m_ct.alphas.push_back(ack->pseudo_out_alpha());
    m_ct.spend_encs.push_back(ack->spend_key());
  }

  void Signer::sort_ki(){
    const size_t input_size = cur_tx().sources.size();

    m_ct.source_permutation.clear();
    for (size_t n = 0; n < input_size; ++n){
      m_ct.source_permutation.push_back(n);
    }

    CHECK_AND_ASSERT_THROW_MES(m_ct.tx.vin.size() == input_size, "Invalid vector size");
    std::sort(m_ct.source_permutation.begin(), m_ct.source_permutation.end(), [&](const size_t i0, const size_t i1) {
      const cryptonote::txin_to_key &tk0 = boost::get<cryptonote::txin_to_key>(m_ct.tx.vin[i0]);
      const cryptonote::txin_to_key &tk1 = boost::get<cryptonote::txin_to_key>(m_ct.tx.vin[i1]);
      return memcmp(&tk0.k_image, &tk1.k_image, sizeof(tk0.k_image)) > 0;
    });

    CHECK_AND_ASSERT_THROW_MES(m_ct.tx_in_hmacs.size() == input_size, "Invalid vector size");
    CHECK_AND_ASSERT_THROW_MES(m_ct.pseudo_outs.size() == input_size, "Invalid vector size");
    CHECK_AND_ASSERT_THROW_MES(m_ct.pseudo_outs_hmac.size() == input_size, "Invalid vector size");
    CHECK_AND_ASSERT_THROW_MES(m_ct.alphas.size() == input_size, "Invalid vector size");
    CHECK_AND_ASSERT_THROW_MES(m_ct.spend_encs.size() == input_size, "Invalid vector size");
    CHECK_AND_ASSERT_THROW_MES(m_ct.tx_data.sources.size() == input_size, "Invalid vector size");

    tools::apply_permutation(m_ct.source_permutation, [&](size_t i0, size_t i1){
      std::swap(m_ct.tx.vin[i0], m_ct.tx.vin[i1]);
      std::swap(m_ct.tx_in_hmacs[i0], m_ct.tx_in_hmacs[i1]);
      std::swap(m_ct.pseudo_outs[i0], m_ct.pseudo_outs[i1]);
      std::swap(m_ct.pseudo_outs_hmac[i0], m_ct.pseudo_outs_hmac[i1]);
      std::swap(m_ct.alphas[i0], m_ct.alphas[i1]);
      std::swap(m_ct.spend_encs[i0], m_ct.spend_encs[i1]);
      std::swap(m_ct.tx_data.sources[i0], m_ct.tx_data.sources[i1]);
    });
  }

  std::shared_ptr<messages::monero::MoneroTransactionInputsPermutationRequest> Signer::step_permutation(){
    sort_ki();

    auto res = std::make_shared<messages::monero::MoneroTransactionInputsPermutationRequest>();
    assign_to_repeatable(res->mutable_perm(), m_ct.source_permutation.begin(), m_ct.source_permutation.end());

    return res;
  }

  void Signer::step_permutation_ack(std::shared_ptr<const messages::monero::MoneroTransactionInputsPermutationAck> ack){

  }

  std::shared_ptr<messages::monero::MoneroTransactionInputViniRequest> Signer::step_set_vini_input(size_t idx){
    CHECK_AND_ASSERT_THROW_MES(idx < m_ct.tx_data.sources.size(), "Invalid transaction index");
    CHECK_AND_ASSERT_THROW_MES(idx < m_ct.tx.vin.size(), "Invalid transaction index");
    CHECK_AND_ASSERT_THROW_MES(idx < m_ct.tx_in_hmacs.size(), "Invalid transaction index");

    m_ct.cur_input_idx = idx;
    auto tx = m_ct.tx_data;
    auto res = std::make_shared<messages::monero::MoneroTransactionInputViniRequest>();
    auto & vini = m_ct.tx.vin[idx];
    translate_src_entry(res->mutable_src_entr(), &(tx.sources[idx]));
    res->set_vini(cryptonote::t_serializable_object_to_blob(vini));
    res->set_vini_hmac(m_ct.tx_in_hmacs[idx]);

    if (client_version() == 0) {
      CHECK_AND_ASSERT_THROW_MES(idx < m_ct.pseudo_outs.size(), "Invalid transaction index");
      CHECK_AND_ASSERT_THROW_MES(idx < m_ct.pseudo_outs_hmac.size(), "Invalid transaction index");
      res->set_pseudo_out(m_ct.pseudo_outs[idx]);
      res->set_pseudo_out_hmac(m_ct.pseudo_outs_hmac[idx]);
    }

    return res;
  }

  void Signer::step_set_vini_input_ack(std::shared_ptr<const messages::monero::MoneroTransactionInputViniAck> ack){

  }

  std::shared_ptr<messages::monero::MoneroTransactionAllInputsSetRequest> Signer::step_all_inputs_set(){
    return std::make_shared<messages::monero::MoneroTransactionAllInputsSetRequest>();
  }

  void Signer::step_all_inputs_set_ack(std::shared_ptr<const messages::monero::MoneroTransactionAllInputsSetAck> ack){
    if (client_version() > 0 || !is_offloading()){
      return;
    }

    // If offloading, expect rsig configuration.
    if (!ack->has_rsig_data()){
      throw exc::ProtocolException("Rsig offloading requires rsig param");
    }

    auto & rsig_data = ack->rsig_data();
    if (!rsig_data.has_mask()){
      throw exc::ProtocolException("Gamma masks not present in offloaded version");
    }

    auto & mask = rsig_data.mask();
    if (mask.size() != 32 * num_outputs()){
      throw exc::ProtocolException("Invalid number of gamma masks");
    }

    m_ct.rsig_gamma.reserve(num_outputs());
    for(size_t c=0; c < num_outputs(); ++c){
      rct::key cmask{};
      memcpy(cmask.bytes, mask.data() + c * 32, 32);
      m_ct.rsig_gamma.emplace_back(cmask);
    }
  }

  std::shared_ptr<messages::monero::MoneroTransactionSetOutputRequest> Signer::step_set_output(size_t idx){
    CHECK_AND_ASSERT_THROW_MES(idx < m_ct.tx_data.splitted_dsts.size(), "Invalid transaction index");
    CHECK_AND_ASSERT_THROW_MES(idx < m_ct.tx_out_entr_hmacs.size(), "Invalid transaction index");
    CHECK_AND_ASSERT_THROW_MES(is_req_bulletproof(), "Borromean rsig not supported");

    m_ct.cur_output_idx = idx;
    m_ct.cur_output_in_batch_idx += 1;   // assumes sequential call to step_set_output()

    auto res = std::make_shared<messages::monero::MoneroTransactionSetOutputRequest>();
    auto & cur_dst = m_ct.tx_data.splitted_dsts[idx];
    translate_dst_entry(res->mutable_dst_entr(), &cur_dst);
    res->set_dst_entr_hmac(m_ct.tx_out_entr_hmacs[idx]);

    // Range sig offloading to the host
    // ClientV0 sends offloaded BP with the last message in the batch.
    // ClientV1 needs additional message after the last message in the batch as BP uses deterministic masks.
    if (client_version() == 0 && is_offloading() && should_compute_bp_now()) {
      auto rsig_data = res->mutable_rsig_data();
      compute_bproof(*rsig_data);
    }

    return res;
  }

  void Signer::step_set_output_ack(std::shared_ptr<const messages::monero::MoneroTransactionSetOutputAck> ack){
    cryptonote::tx_out tx_out;
    rct::Bulletproof bproof{};
    rct::ctkey out_pk{};
    rct::ecdhTuple ecdh{};

    bool has_rsig = false;
    std::string rsig_buff;

    if (ack->has_rsig_data()){
      auto & rsig_data = ack->rsig_data();

      if (rsig_data.has_rsig() && !rsig_data.rsig().empty()){
        has_rsig = true;
        rsig_buff = rsig_data.rsig();
      }

      if (client_version() >= 1 && rsig_data.has_mask()){
        rct::key cmask{};
        string_to_key(cmask, rsig_data.mask());
        m_ct.rsig_gamma.emplace_back(cmask);
      }
    }

    if (!cn_deserialize(ack->tx_out(), tx_out)){
      throw exc::ProtocolException("Cannot deserialize vout[i]");
    }

    if (!cn_deserialize(ack->out_pk(), out_pk)){
      throw exc::ProtocolException("Cannot deserialize out_pk");
    }

    if (m_ct.bp_version <= 1) {
      if (!cn_deserialize(ack->ecdh_info(), ecdh)){
        throw exc::ProtocolException("Cannot deserialize ecdhtuple");
      }
    } else {
      CHECK_AND_ASSERT_THROW_MES(8 == ack->ecdh_info().size(), "Invalid ECDH.amount size");
      memcpy(ecdh.amount.bytes, ack->ecdh_info().data(), 8);
    }

    if (has_rsig && is_req_bulletproof() && !cn_deserialize(rsig_buff, bproof)){
      throw exc::ProtocolException("Cannot deserialize bulletproof rangesig");
    }

    m_ct.tx.vout.emplace_back(tx_out);
    m_ct.tx_out_hmacs.push_back(ack->vouti_hmac());
    m_ct.tx_out_pk.emplace_back(out_pk);
    m_ct.tx_out_ecdh.emplace_back(ecdh);

    // ClientV0, if no rsig was generated on Trezor, do not continue.
    // ClientV1+ generates BP after all masks in the current batch are generated
    if (!has_rsig || (client_version() >= 1 && is_offloading())){
      return;
    }

    process_bproof(bproof);
    m_ct.cur_batch_idx += 1;
    m_ct.cur_output_in_batch_idx = 0;
  }

  bool Signer::should_compute_bp_now() const {
    CHECK_AND_ASSERT_THROW_MES(m_ct.cur_batch_idx < m_ct.grouping_vct.size(), "Invalid batch index");
    return m_ct.grouping_vct[m_ct.cur_batch_idx] <= m_ct.cur_output_in_batch_idx;
  }

  void Signer::compute_bproof(messages::monero::MoneroTransactionRsigData & rsig_data){
    auto batch_size = m_ct.grouping_vct[m_ct.cur_batch_idx];
    std::vector<uint64_t> amounts;
    rct::keyV masks;
    CHECK_AND_ASSERT_THROW_MES(m_ct.cur_output_idx + 1 >= batch_size, "Invalid index for batching");

    for(size_t i = 0; i < batch_size; ++i){
      const size_t bidx = 1 + m_ct.cur_output_idx - batch_size + i;
      CHECK_AND_ASSERT_THROW_MES(bidx < m_ct.tx_data.splitted_dsts.size(), "Invalid gamma index");
      CHECK_AND_ASSERT_THROW_MES(bidx < m_ct.rsig_gamma.size(), "Invalid gamma index");

      amounts.push_back(m_ct.tx_data.splitted_dsts[bidx].amount);
      masks.push_back(m_ct.rsig_gamma[bidx]);
    }

    auto bp = bulletproof_PROVE(amounts, masks);
    auto serRsig = cn_serialize(bp);
    m_ct.tx_out_rsigs.emplace_back(bp);
    rsig_data.set_rsig(serRsig);
  }

  void Signer::process_bproof(rct::Bulletproof & bproof){
    CHECK_AND_ASSERT_THROW_MES(m_ct.cur_batch_idx < m_ct.grouping_vct.size(), "Invalid batch index");
    auto batch_size = m_ct.grouping_vct[m_ct.cur_batch_idx];
    for (size_t i = 0; i < batch_size; ++i){
      const size_t bidx = 1 + m_ct.cur_output_idx - batch_size + i;
      CHECK_AND_ASSERT_THROW_MES(bidx < m_ct.tx_out_pk.size(), "Invalid out index");

      rct::key commitment = m_ct.tx_out_pk[bidx].mask;
      commitment = rct::scalarmultKey(commitment, rct::INV_EIGHT);
      bproof.V.push_back(commitment);
    }

    m_ct.tx_out_rsigs.emplace_back(bproof);
    if (!rct::bulletproof_VERIFY(boost::get<rct::Bulletproof>(m_ct.tx_out_rsigs.back()))) {
      throw exc::ProtocolException("Returned range signature is invalid");
    }
  }

  std::shared_ptr<messages::monero::MoneroTransactionSetOutputRequest> Signer::step_rsig(size_t idx){
    if (client_version() == 0 || !is_offloading() || !should_compute_bp_now()){
      return nullptr;
    }

    auto res = std::make_shared<messages::monero::MoneroTransactionSetOutputRequest>();
    auto & cur_dst = m_ct.tx_data.splitted_dsts[idx];
    translate_dst_entry(res->mutable_dst_entr(), &cur_dst);
    res->set_dst_entr_hmac(m_ct.tx_out_entr_hmacs[idx]);

    compute_bproof(*(res->mutable_rsig_data()));
    res->set_is_offloaded_bp(true);
    return res;
  }

  void Signer::step_set_rsig_ack(std::shared_ptr<const messages::monero::MoneroTransactionSetOutputAck> ack){
    m_ct.cur_batch_idx += 1;
    m_ct.cur_output_in_batch_idx = 0;
  }

  std::shared_ptr<messages::monero::MoneroTransactionAllOutSetRequest> Signer::step_all_outs_set(){
    return std::make_shared<messages::monero::MoneroTransactionAllOutSetRequest>();
  }

  void Signer::step_all_outs_set_ack(std::shared_ptr<const messages::monero::MoneroTransactionAllOutSetAck> ack, hw::device &hwdev){
    m_ct.rv = std::make_shared<rct::rctSig>();
    m_ct.rv->txnFee = ack->rv().txn_fee();
    m_ct.rv->type = static_cast<uint8_t>(ack->rv().rv_type());
    string_to_key(m_ct.rv->message, ack->rv().message());

    // Extra copy
    m_ct.tx.extra.clear();
    auto extra = ack->extra();
    auto extra_data = extra.data();
    m_ct.tx.extra.reserve(extra.size());
    for(size_t i = 0; i < extra.size(); ++i){
      m_ct.tx.extra.push_back(static_cast<uint8_t>(extra_data[i]));
    }

    ::crypto::hash tx_prefix_hash{};
    cryptonote::get_transaction_prefix_hash(m_ct.tx, tx_prefix_hash);
    m_ct.tx_prefix_hash = key_to_string(tx_prefix_hash);
    if (crypto_verify_32(reinterpret_cast<const unsigned char *>(tx_prefix_hash.data),
        reinterpret_cast<const unsigned char *>(ack->tx_prefix_hash().data()))){
      throw exc::proto::SecurityException("Transaction prefix has does not match to the computed value");
    }

    // RctSig
    auto num_sources = m_ct.tx_data.sources.size();
    if (is_simple() || is_req_bulletproof()){
      auto dst = &m_ct.rv->pseudoOuts;
      if (is_bulletproof()){
        dst = &m_ct.rv->p.pseudoOuts;
      }

      dst->clear();
      for (const auto &pseudo_out : m_ct.pseudo_outs) {
        dst->emplace_back();
        string_to_key(dst->back(), pseudo_out);
      }

      m_ct.rv->mixRing.resize(num_sources);
    } else {
      m_ct.rv->mixRing.resize(m_ct.tsx_data.mixin());
      m_ct.rv->mixRing[0].resize(num_sources);
    }

    CHECK_AND_ASSERT_THROW_MES(m_ct.tx_out_pk.size() == m_ct.tx_out_ecdh.size(), "Invalid vector sizes");
    for(size_t i = 0; i < m_ct.tx_out_ecdh.size(); ++i){
      m_ct.rv->outPk.push_back(m_ct.tx_out_pk[i]);
      m_ct.rv->ecdhInfo.push_back(m_ct.tx_out_ecdh[i]);
    }

    for(size_t i = 0; i < m_ct.tx_out_rsigs.size(); ++i){
      if (is_bulletproof()){
        m_ct.rv->p.bulletproofs.push_back(boost::get<rct::Bulletproof>(m_ct.tx_out_rsigs[i]));
      } else {
        m_ct.rv->p.rangeSigs.push_back(boost::get<rct::rangeSig>(m_ct.tx_out_rsigs[i]));
      }
    }

    rct::key hash_computed = rct::get_pre_mlsag_hash(*(m_ct.rv), hwdev);
    auto & hash = ack->full_message_hash();

    if (hash.size() != 32){
      throw exc::ProtocolException("Returned mlsag hash has invalid size");
    }

    if (crypto_verify_32(reinterpret_cast<const unsigned char *>(hash_computed.bytes),
                         reinterpret_cast<const unsigned char *>(hash.data()))){
      throw exc::proto::SecurityException("Computed MLSAG does not match");
    }
  }

  std::shared_ptr<messages::monero::MoneroTransactionSignInputRequest> Signer::step_sign_input(size_t idx){
    m_ct.cur_input_idx = idx;

    CHECK_AND_ASSERT_THROW_MES(idx < m_ct.tx_data.sources.size(), "Invalid transaction index");
    CHECK_AND_ASSERT_THROW_MES(idx < m_ct.tx.vin.size(), "Invalid transaction index");
    CHECK_AND_ASSERT_THROW_MES(idx < m_ct.tx_in_hmacs.size(), "Invalid transaction index");
    CHECK_AND_ASSERT_THROW_MES(idx < m_ct.alphas.size(), "Invalid transaction index");
    CHECK_AND_ASSERT_THROW_MES(idx < m_ct.spend_encs.size(), "Invalid transaction index");

    auto res = std::make_shared<messages::monero::MoneroTransactionSignInputRequest>();
    translate_src_entry(res->mutable_src_entr(), &(m_ct.tx_data.sources[idx]));
    res->set_vini(cryptonote::t_serializable_object_to_blob(m_ct.tx.vin[idx]));
    res->set_vini_hmac(m_ct.tx_in_hmacs[idx]);
    res->set_pseudo_out_alpha(m_ct.alphas[idx]);
    res->set_spend_key(m_ct.spend_encs[idx]);

    CHECK_AND_ASSERT_THROW_MES(idx < m_ct.pseudo_outs.size(), "Invalid transaction index");
    CHECK_AND_ASSERT_THROW_MES(idx < m_ct.pseudo_outs_hmac.size(), "Invalid transaction index");
    res->set_pseudo_out(m_ct.pseudo_outs[idx]);
    res->set_pseudo_out_hmac(m_ct.pseudo_outs_hmac[idx]);
    return res;
  }

  void Signer::step_sign_input_ack(std::shared_ptr<const messages::monero::MoneroTransactionSignInputAck> ack){
    rct::mgSig mg;
    if (!cn_deserialize(ack->signature(), mg)){
      throw exc::ProtocolException("Cannot deserialize mg[i]");
    }

    // Sync updated pseudo_outputs, client_version>=1, HF10+
    if (client_version() >= 1 && ack->has_pseudo_out()){
      CHECK_AND_ASSERT_THROW_MES(m_ct.cur_input_idx < m_ct.pseudo_outs.size(), "Invalid pseudo-out index");
      m_ct.pseudo_outs[m_ct.cur_input_idx] = ack->pseudo_out();
      if (is_bulletproof()){
        CHECK_AND_ASSERT_THROW_MES(m_ct.cur_input_idx < m_ct.rv->p.pseudoOuts.size(), "Invalid pseudo-out index");
        string_to_key(m_ct.rv->p.pseudoOuts[m_ct.cur_input_idx], ack->pseudo_out());
      } else {
        CHECK_AND_ASSERT_THROW_MES(m_ct.cur_input_idx < m_ct.rv->pseudoOuts.size(), "Invalid pseudo-out index");
        string_to_key(m_ct.rv->pseudoOuts[m_ct.cur_input_idx], ack->pseudo_out());
      }
    }

    m_ct.rv->p.MGs.push_back(mg);
  }

  std::shared_ptr<messages::monero::MoneroTransactionFinalRequest> Signer::step_final(){
    m_ct.tx.rct_signatures = *(m_ct.rv);
    return std::make_shared<messages::monero::MoneroTransactionFinalRequest>();
  }

  void Signer::step_final_ack(std::shared_ptr<const messages::monero::MoneroTransactionFinalAck> ack){
    if (m_multisig){
      auto & cout_key = ack->cout_key();
      for(auto & cur : m_ct.couts){
        if (cur.size() != crypto::chacha::IV_SIZE + 32){
          throw std::invalid_argument("Encrypted cout has invalid length");
        }

        char buff[32];
        auto data = cur.data();

        crypto::chacha::decrypt(data + crypto::chacha::IV_SIZE, 32, reinterpret_cast<const uint8_t *>(cout_key.data()), reinterpret_cast<const uint8_t *>(data), buff);
        m_ct.couts_dec.emplace_back(buff, 32);
      }
    }

    m_ct.enc_salt1 = ack->salt();
    m_ct.enc_salt2 = ack->rand_mult();
    m_ct.enc_keys = ack->tx_enc_keys();
  }

  std::string Signer::store_tx_aux_info(){
    rapidjson::StringBuffer sb;
    rapidjson::Writer<rapidjson::StringBuffer> writer(sb);

    rapidjson::Document json;
    json.SetObject();

    rapidjson::Value valueS(rapidjson::kStringType);
    rapidjson::Value valueI(rapidjson::kNumberType);

    valueI.SetInt(1);
    json.AddMember("version", valueI, json.GetAllocator());

    valueS.SetString(m_ct.enc_salt1.c_str(), m_ct.enc_salt1.size());
    json.AddMember("salt1", valueS, json.GetAllocator());

    valueS.SetString(m_ct.enc_salt2.c_str(), m_ct.enc_salt2.size());
    json.AddMember("salt2", valueS, json.GetAllocator());

    valueS.SetString(m_ct.tx_prefix_hash.c_str(), m_ct.tx_prefix_hash.size());
    json.AddMember("tx_prefix_hash", valueS, json.GetAllocator());

    valueS.SetString(m_ct.enc_keys.c_str(), m_ct.enc_keys.size());
    json.AddMember("enc_keys", valueS, json.GetAllocator());

    json.Accept(writer);
    return sb.GetString();
  }

  void load_tx_key_data(hw::device_cold::tx_key_data_t & res, const std::string & data)
  {
    rapidjson::Document json;

    // The contents should be JSON if the wallet follows the new format.
    if (json.Parse(data.c_str()).HasParseError())
    {
      throw std::invalid_argument("Data parsing error");
    }
    else if(!json.IsObject())
    {
      throw std::invalid_argument("Data parsing error - not an object");
    }

    GET_FIELD_FROM_JSON(json, version, int, Int, true, -1);
    GET_STRING_FROM_JSON(json, salt1, std::string, true, std::string());
    GET_STRING_FROM_JSON(json, salt2, std::string, true, std::string());
    GET_STRING_FROM_JSON(json, enc_keys, std::string, true, std::string());
    GET_STRING_FROM_JSON(json, tx_prefix_hash, std::string, false, std::string());

    if (field_version != 1)
    {
      throw std::invalid_argument("Unknown version");
    }

    res.salt1 = field_salt1;
    res.salt2 = field_salt2;
    res.tx_enc_keys = field_enc_keys;
    res.tx_prefix_hash = field_tx_prefix_hash;
  }

  std::shared_ptr<messages::monero::MoneroGetTxKeyRequest> get_tx_key(
      const hw::device_cold::tx_key_data_t & tx_data)
  {
    auto req = std::make_shared<messages::monero::MoneroGetTxKeyRequest>();
    req->set_salt1(tx_data.salt1);
    req->set_salt2(tx_data.salt2);
    req->set_tx_enc_keys(tx_data.tx_enc_keys);
    req->set_tx_prefix_hash(tx_data.tx_prefix_hash);
    req->set_reason(0);

    return req;
  }

  void get_tx_key_ack(
      std::vector<::crypto::secret_key> & tx_keys,
      const std::string & tx_prefix_hash,
      const ::crypto::secret_key & view_key_priv,
      std::shared_ptr<const messages::monero::MoneroGetTxKeyAck> ack
  )
  {
    auto enc_key = protocol::tx::compute_enc_key(view_key_priv, tx_prefix_hash, ack->salt());
    auto & encrypted_keys = ack->has_tx_derivations() ? ack->tx_derivations() : ack->tx_keys();

    const size_t len_ciphertext = encrypted_keys.size();  // IV || keys || TAG
    CHECK_AND_ASSERT_THROW_MES(len_ciphertext > crypto::chacha::IV_SIZE + crypto::chacha::TAG_SIZE, "Invalid size");

    size_t keys_len = len_ciphertext - crypto::chacha::IV_SIZE - crypto::chacha::TAG_SIZE;
    std::unique_ptr<uint8_t[]> plaintext(new uint8_t[keys_len]);

    protocol::crypto::chacha::decrypt(
        encrypted_keys.data() + crypto::chacha::IV_SIZE,
        len_ciphertext - crypto::chacha::IV_SIZE,
        reinterpret_cast<const uint8_t *>(enc_key.data),
        reinterpret_cast<const uint8_t *>(encrypted_keys.data()),
        reinterpret_cast<char *>(plaintext.get()), &keys_len);

    CHECK_AND_ASSERT_THROW_MES(keys_len % 32 == 0, "Invalid size");
    tx_keys.resize(keys_len / 32);

    for(unsigned i = 0; i < keys_len / 32; ++i)
    {
      memcpy(tx_keys[i].data, plaintext.get() + 32 * i, 32);
    }
    memwipe(plaintext.get(), keys_len);
  }

}
}
}
}


/*

Copyright (c) 2005-2018, University of Oxford.
All rights reserved.

University of Oxford means the Chancellor, Masters and Scholars of the
University of Oxford, having an administrative office at Wellington
Square, Oxford OX1 2JD, UK.

This file is part of Chaste.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
 * Redistributions of source code must retain the above copyright notice,
   this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.
 * Neither the name of the University of Oxford nor the names of its
   contributors may be used to endorse or promote products derived from this
   software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

*/

#ifndef ABSTRACTNUMERICALMETHOD_HPP_
#define ABSTRACTNUMERICALMETHOD_HPP_

#include <boost/shared_ptr.hpp>
#include "ChasteSerialization.hpp"
#include "ClassIsAbstract.hpp"
#include "Identifiable.hpp"

#include "AbstractOffLatticeCellPopulation.hpp"
#include "AbstractForce.hpp"

/**
 * An abstract class representing a numerical method for off lattice cell based simulations.
 *
 * Numerical methods have access to the cell population and the force collection.
 * The method is then responsible for evaluating forces at whatever times and positions
 * are required, then updating all node positions.
 */
template<unsigned ELEMENT_DIM, unsigned SPACE_DIM=ELEMENT_DIM>
class AbstractNumericalMethod : public Identifiable
{
    friend class TestNumericalMethods;

private:

    /** Needed for serialization. */
    friend class boost::serialization::access;

    /**
     * Save or restore the simulation.
     *
     * @param archive the archive
     * @param version the current version of this class
     */
    template<class Archive>
    void serialize(Archive & archive, const unsigned int version)
    {
        archive & mUseAdaptiveTimestep;
        archive & mUseUpdateNodeLocation;
        archive & mGhostNodeForcesEnabled;
    }

protected:

    /** Pointer to the cell population being updated by this method*/
    AbstractOffLatticeCellPopulation<ELEMENT_DIM,SPACE_DIM>* mpCellPopulation;

    /** Pointer to the force collection to apply*/
    std::vector<boost::shared_ptr<AbstractForce<ELEMENT_DIM, SPACE_DIM> > >* mpForceCollection;

    /**
     * Whether the numerical method uses an adaptive time step.
     * Initialized to false in the AbstractNumericalMethod constructor.
     */
    bool mUseAdaptiveTimestep;

    /**
     * A boolean flag indicating whether non-forward Euler methods are supported for this type of cell
     * population. Allows us to fall back to the old method of updating node positions for populations
     * that require it (This is only for NodeBasedCellPopulationWithBuskeUpdates).
     *
     * Initialized to false in the AbstractNumericalMethod constructor.
     *
     * \todo #2087 Consider replacing this with static_casts
     */
    bool mUseUpdateNodeLocation;

    /**
     * A boolean indicating whether this cell population type contains ghost nodes.
     * Initialized to true in the AbstractNumericalMethod constructor.
     */
    bool mGhostNodeForcesEnabled;

    /**
     * Computes and returns the force on each node, including the damping factor
     * @return A vector of applied forces
     */
    std::vector<c_vector<double, SPACE_DIM> > ComputeForcesIncludingDamping();

    /**
     * Saves the current location of each cell in the population in a vector.
     * @return A vector of cell positions
     */
    std::vector<c_vector<double, SPACE_DIM> > SaveCurrentLocations();

    /**
     * Updates a single node's position, taking into account periodic boundary conditions
     *
     * @param nodeIndex Index of the node to update
     * @param newPosition C_vector holding the new node position
     */
     void SafeNodePositionUpdate(unsigned nodeIndex, c_vector<double, SPACE_DIM> newPosition);

    /**
     * Detects whether a node has exceeded the acceptable displacement for one timestep.
     * If a step size exception has occurred, it either causes the simulation to terminate or,
     * in adaptive simulations, the exception is caught and the step size is reduced in response.
     *
     * @param nodeIndex Index of the node being examined
     * @param displacement Displacement of the node this step
     * @param dt Time step size
     */
    void DetectStepSizeExceptions(unsigned nodeIndex, c_vector<double,SPACE_DIM>& displacement, double dt);

public:

    /**
     * Constructor.
     * No input parameters are required, allowing the numerical method to be created first, then passed to
     * the simulation. The cell population and force collection pointers are then set by the simulation in
     * its constructor.
     */
    AbstractNumericalMethod<ELEMENT_DIM,SPACE_DIM>();

    /**
     * Destructor.
     */
    virtual ~AbstractNumericalMethod<ELEMENT_DIM,SPACE_DIM>();

    /**
     * Sets the pointer to the cell population updated by this method
     *
     * @param pPopulation Pointer to an AbstractOffLattice cell population
     */
    void SetCellPopulation(AbstractOffLatticeCellPopulation<ELEMENT_DIM,SPACE_DIM>* pPopulation);

    /**
     * Sets the pointer to the force collection applied by this method
     *
     * @param pForces Pointer to the simulation's force collection
     */
    void SetForceCollection(std::vector<boost::shared_ptr<AbstractForce<ELEMENT_DIM, SPACE_DIM> > >* pForces);

    /**
     * Set mUseAdaptiveTimestep.
     *
     * @param useAdaptiveTimestep whether to use an adaptive time step
     */
    void SetUseAdaptiveTimestep(bool useAdaptiveTimestep);

    /**
     * Set mUseUpdateNodeLocation.
     *
     * @param useUpdateNodeLocation whether to use the population method UpdateNodeLocations()
     */
    void SetUseUpdateNodeLocation(bool useUpdateNodeLocation);

    /**
     * Get mUseUpdateNodeLocation.
     *
     * @return whether the population method UpdateNodeLocations() is being used
     */
    bool GetUseUpdateNodeLocation();

    /**
     * @return whether the numerical method uses an adaptive time step.
     */
    bool HasAdaptiveTimestep();

    /**
     * Updates node positions according to Newton's 2nd law with overdamping.
     *
     * As this method is pure virtual, it must be overridden
     * in subclasses.
     *
     * @param dt Time step size
     */
    virtual void UpdateAllNodePositions(double dt)=0;

    /**
     * Saves the name of the numerical method to the parameters file
     *
     * @param rParamsFile Reference to the parameter output filestream
     */
    void OutputNumericalMethodInfo(out_stream& rParamsFile);

    /**
     * Saves any additional numerical method details to the parameters file.
     *
     * @param rParamsFile Reference to the parameter output filestream
     */
    virtual void OutputNumericalMethodParameters(out_stream& rParamsFile);
};

TEMPLATED_CLASS_IS_ABSTRACT_2_UNSIGNED(AbstractNumericalMethod)

#endif /*ABSTRACTNUMERICALMETHOD_HPP_*/


/*
 * Gamedev Framework (gf)
 * Copyright (C) 2016-2022 Julien Bernard
 *
 * This software is provided 'as-is', without any express or implied
 * warranty.  In no event will the authors be held liable for any damages
 * arising from the use of this software.
 *
 * Permission is granted to anyone to use this software for any purpose,
 * including commercial applications, and to alter it and redistribute it
 * freely, subject to the following restrictions:
 *
 * 1. The origin of this software must not be misrepresented; you must not
 *    claim that you wrote the original software. If you use this software
 *    in a product, an acknowledgment in the product documentation would be
 *    appreciated but is not required.
 * 2. Altered source versions must be plainly marked as such, and must not be
 *    misrepresented as being the original software.
 * 3. This notice may not be removed or altered from any source distribution.
 */
#include <gf/Map.h>

#include <iostream>
#include <limits>

#include <gf/Geometry.h>
#include <gf/VectorOps.h>

#include <boost/heap/binomial_heap.hpp>

namespace gf {
#ifndef DOXYGEN_SHOULD_SKIP_THIS
inline namespace v1 {
#endif

  SquareMap::SquareMap(Vector2i size)
  : m_cells(size, None)
  {

  }

  Vector2i SquareMap::getSize() const {
    return m_cells.getSize();
  }

  PositionRange<int> SquareMap::getRange() const {
    return m_cells.getPositionRange();
  }

  void SquareMap::setCell(Vector2i pos, Flags<CellProperty> flags) {
    m_cells(pos) = flags;
  }

  void SquareMap::reset(Flags<CellProperty> flags) {
    for (auto& cell : m_cells) {
      cell = flags;
    }
  }

  void SquareMap::setTransparent(Vector2i pos, bool transparent) {
    if (transparent) {
      m_cells(pos).set(CellProperty::Transparent);
    } else {
      m_cells(pos).reset(CellProperty::Transparent);
    }
  }

  bool SquareMap::isTransparent(Vector2i pos) const {
    return m_cells(pos).test(CellProperty::Transparent);
  }

  void SquareMap::setWalkable(Vector2i pos, bool walkable) {
    if (walkable) {
      m_cells(pos).set(CellProperty::Walkable);
    } else {
      m_cells(pos).reset(CellProperty::Walkable);
    }
  }

  bool SquareMap::isWalkable(Vector2i pos) const {
    return m_cells(pos).test(CellProperty::Walkable);
  }

  void SquareMap::setEmpty(Vector2i pos) {
    m_cells(pos) = EmptyCell;
  }

  /*
   * FoV
   */

  void SquareMap::clearFieldOfVision() {
    for (auto& cell : m_cells) {
      cell.reset(CellProperty::Visible);
    }
  }

  void SquareMap::clearExplored() {
    for (auto& cell : m_cells) {
      cell.reset(CellProperty::Explored);
    }
  }

  namespace {

    void postProcessMap(Array2D<Flags<CellProperty>, int>& cells, Vector2i q0, Vector2i q1, Vector2i step) {
      int xLo, xHi, yLo, yHi;
      std::tie(xLo, xHi) = std::minmax(q0.x, q1.x);
      std::tie(yLo, yHi) = std::minmax(q0.y, q1.y);

      for (int y = yLo; y <= yHi; ++y) {
        for (int x = xLo; x <= xHi; ++x) {
          if (!cells.isValid({ x, y })) {
            continue;
          }

          if (!cells({ x, y }).test(CellProperty::Visible) || !cells({ x, y }).test(CellProperty::Transparent)) {
            continue;
          }

          int x2 = x + step.x;
          int y2 = y + step.y;

          if (xLo <= x2 && x2 <= xHi) {
            gf::Vector2i target = { x2, y };

            if (cells.isValid(target) && !cells(target).test(CellProperty::Transparent)) {
              cells(target).set(CellProperty::Visible);
            }
          }

          if (yLo <= y2 && y2 <= yHi) {
            gf::Vector2i target = { x, y2 };

            if (cells.isValid(target) && !cells(target).test(CellProperty::Transparent)) {
              cells(target).set(CellProperty::Visible);
            }
          }

          if (xLo <= x2 && x2 <= xHi && yLo <= y2 && y2 <= yHi) {
            gf::Vector2i target = { x2, y2 };

            if (cells.isValid(target) && !cells(target).test(CellProperty::Transparent)) {
              cells(target).set(CellProperty::Visible);
            }
          }
        }
      }
    }

    void castRay(Array2D<Flags<CellProperty>, int>& cells, Vector2i p0, Vector2i p1, int maxRadius2, FieldOfVisionLimit limit, Flags<CellProperty> modification) {
      Bresenham bresenham(p0, p1);
      Vector2i curr;
      bool blocked = false;

      while (!bresenham.step(curr)) {
        if (!cells.isValid(curr)) {
          return;
        }

        if (maxRadius2 > 0) {
          int radius2 = gf::squareDistance(p0, curr);

          if (radius2 > maxRadius2) {
            return;
          }
        }

        if (!blocked && !cells(curr).test(CellProperty::Transparent)) {
          blocked = true;
        } else if (blocked) {
          return; // wall
        }

        if (limit == FieldOfVisionLimit::Included || !blocked) {
          cells(curr) |= modification;
        }
      }
    }

    void computeBasicFov(Array2D<Flags<CellProperty>, int>& cells, Vector2i pos, int maxRadius, FieldOfVisionLimit limit, Flags<CellProperty> modification) {
      RangeI xRange = cells.getColRange();
      RangeI yRange = cells.getRowRange();

      int maxRadius2 = maxRadius * maxRadius;

      if (maxRadius > 0) {
        xRange.lo = std::max(xRange.lo, pos.x - maxRadius);
        xRange.hi = std::min(xRange.hi, pos.x + maxRadius);
        yRange.lo = std::max(yRange.lo, pos.y - maxRadius);
        yRange.hi = std::min(yRange.hi, pos.y + maxRadius);
      } else {
        maxRadius2 = 0;
      }

      cells(pos) |= modification;

      for (auto x : xRange) {
        castRay(cells, pos, { x, yRange.lo }, maxRadius2, limit, modification);
        castRay(cells, pos, { x, yRange.hi }, maxRadius2, limit, modification);
      }

      for (auto y : yRange) {
        castRay(cells, pos, { xRange.lo, y }, maxRadius2, limit, modification);
        castRay(cells, pos, { xRange.hi, y }, maxRadius2, limit, modification);
      }

      if (limit == FieldOfVisionLimit::Included) {
        postProcessMap(cells, pos, { xRange.lo, yRange.lo }, { -1, -1 });
        postProcessMap(cells, pos, { xRange.hi, yRange.lo }, {  1, -1 });
        postProcessMap(cells, pos, { xRange.lo, yRange.hi }, { -1,  1 });
        postProcessMap(cells, pos, { xRange.hi, yRange.hi }, {  1,  1 });
      }
    }

    void computeGenericFieldOfVision(Array2D<Flags<CellProperty>, int>& cells, Vector2i pos, int maxRadius, FieldOfVisionLimit limit, FieldOfVision algorithm, Flags<CellProperty> modification) {
      switch (algorithm) {
        case FieldOfVision::Basic:
          computeBasicFov(cells, pos, maxRadius, limit, modification);
          break;

        default:
          break;
      }
    }

  } // anonymous namespace

  void SquareMap::computeFieldOfVision(Vector2i pos, int maxRadius, FieldOfVisionLimit limit, FieldOfVision algorithm) {
    computeGenericFieldOfVision(m_cells, pos, maxRadius, limit, algorithm, CellProperty::Visible | CellProperty::Explored);
  }

  void SquareMap::computeLocalFieldOfVision(Vector2i pos, int maxRadius, FieldOfVisionLimit limit, FieldOfVision algorithm) {
    computeGenericFieldOfVision(m_cells, pos, maxRadius, limit, algorithm, CellProperty::Visible);
  }

  bool SquareMap::isInFieldOfVision(Vector2i pos) const {
    return m_cells(pos).test(CellProperty::Visible);
  }

  bool SquareMap::isExplored(Vector2i pos) const {
    return m_cells(pos).test(CellProperty::Explored);
  }

  /*
   * Route
   */

  namespace {

    // Dijkstra

    struct DijkstraHeapData {
      Vector2i position;
      float distance;
    };

    bool operator<(const DijkstraHeapData& lhs, const DijkstraHeapData& rhs) {
      return lhs.distance > rhs.distance;
    }

    using DijkstraHeap = boost::heap::binomial_heap<DijkstraHeapData>;

    struct DijkstraResultData {
      float distance;
      Vector2i previous;
      DijkstraHeap::handle_type handle;
    };

    std::vector<Vector2i> computeDijkstra(const Array2D<Flags<CellProperty>, int>& cells, Vector2i origin, Vector2i target, float diagonalCost) {
      DijkstraResultData defaultResult;
      defaultResult.distance = std::numeric_limits<float>::infinity();
      defaultResult.previous = { -1, -1 };

      Array2D<DijkstraResultData, int> results(cells.getSize(), defaultResult);

      results(origin).distance = 0.0f;

      DijkstraHeap heap;

      for (auto position : cells.getPositionRange()) {
        const auto& cell = cells(position);

        if (!cell.test(CellProperty::Walkable)) {
          continue;
        }

        DijkstraHeapData data;
        data.position = position;
        data.distance = results(position).distance;

        results(position).handle = heap.push(data);
      }

      while (!heap.empty()) {
        DijkstraHeapData data = heap.top();
        heap.pop();

        for (auto position : cells.get8NeighborsRange(data.position)) {
          assert(position != data.position);

          const Flags<CellProperty>& value = cells(position);

          if (!value.test(CellProperty::Walkable)) {
            continue;
          }

          bool isDiagonal = (gf::manhattanDistance(data.position, position) == 2);

          if (isDiagonal && diagonalCost == 0) {
            continue;
          }

          float newDistance = results(data.position).distance + (isDiagonal ? diagonalCost : 1.0f);

          if (newDistance < results(position).distance) {
            auto& result = results(position);
            result.distance = newDistance;
            result.previous = data.position;

            assert((*result.handle).position == position);
            (*result.handle).distance = newDistance;
            heap.increase(result.handle);
          }
        }
      }

      std::vector<Vector2i> route;
      Vector2i curr = target;

      while (curr != origin) {
        if (curr.x == -1 || curr.y == -1) {
          return {};
        }
        route.push_back(curr);
        curr = results(curr).previous;
      }

      route.push_back(origin);
      std::reverse(route.begin(), route.end());

      assert(!route.empty());

      return route;
    }

    // AStar

    struct AStarHeapData {
      Vector2i position;
      float priority;
    };

    bool operator<(const AStarHeapData& lhs, const AStarHeapData& rhs) {
      return lhs.priority > rhs.priority;
    }

    using AStarHeap = boost::heap::binomial_heap<AStarHeapData>;

    enum class AStarState {
      None,
      Open,
      Closed,
    };

    struct AStarResultData {
      float distance;
      Vector2i previous;
      AStarState state;
      AStarHeap::handle_type handle;
    };

    std::vector<Vector2i> computeAStar(const Array2D<Flags<CellProperty>, int>& cells, Vector2i origin, Vector2i target, float diagonalCost) {
      AStarResultData defaultResult;
      defaultResult.distance = std::numeric_limits<float>::infinity();
      defaultResult.previous = { -1, -1 };
      defaultResult.state = AStarState::None;

      Array2D<AStarResultData, int> results(cells.getSize(), defaultResult);

      results(origin).distance = 0.0f;
      results(origin).state = AStarState::Open;

      AStarHeap heap;
      results(origin).handle = heap.push({ origin, 0.0f });

      // see http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html#heuristics-for-grid-maps
      auto heuristic = [diagonalCost](Vector2i p0, Vector2i p1) {
        if (diagonalCost == 0) {
          return 1.0f * gf::manhattanDistance(p0, p1);
        }

        Vector2i d = gf::abs(p0 - p1);
        return 1.0f * (d.x + d.y) + (diagonalCost - 2.0f) * std::min(d.x, d.y);
      };

      while (!heap.empty()) {
        AStarHeapData data = heap.top();
        heap.pop();

        assert(results(data.position).state == AStarState::Open);

        if (data.position == target) {
          break;
        }

        results(data.position).state = AStarState::Closed;

        for (auto position : cells.get8NeighborsRange(data.position)) {
          assert(position != data.position);

          const Flags<CellProperty>& value = cells(position);

          if (!value.test(CellProperty::Walkable)) {
            continue;
          }

          if (results(position).state == AStarState::Closed) {
            continue;
          }

          bool isDiagonal = (gf::manhattanDistance(data.position, position) == 2);

          if (isDiagonal && diagonalCost == 0) {
            continue;
          }

          float newDistance = results(data.position).distance + (isDiagonal ? diagonalCost : 1.0f);

          if (newDistance < results(position).distance) {
            auto& result = results(position);
            result.distance = newDistance;
            result.previous = data.position;

            float priority = newDistance + heuristic(position, target) * 1.001f;

            if (result.state == AStarState::Open) {
              assert((*result.handle).position == position);

              if ((*result.handle).priority != priority) {
                (*result.handle).priority = priority;
                heap.update(result.handle);
              }
            } else {
              assert(result.state == AStarState::None);
              result.handle = heap.push({ position, priority });
              result.state = AStarState::Open;
            }
          }
        }
      }

      std::vector<Vector2i> route;
      Vector2i curr = target;

      while (curr != origin) {
        if (curr.x == -1 || curr.y == -1) {
          return {};
        }

        route.push_back(curr);
        curr = results(curr).previous;
      }

      route.push_back(origin);
      std::reverse(route.begin(), route.end());

      assert(!route.empty());

      return route;
    }

  } // anonymous namespace

  std::vector<Vector2i> SquareMap::computeRoute(Vector2i origin, Vector2i target, float diagonalCost, Route algorithm) {
    switch (algorithm) {
      case Route::Dijkstra:
        return computeDijkstra(m_cells, origin, target, diagonalCost);

      case Route::AStar:
        return computeAStar(m_cells, origin, target, diagonalCost);
    }

    return { };
  }

#ifndef DOXYGEN_SHOULD_SKIP_THIS
}
#endif
}