//==========================================================================
//  AIDA Detector description implementation 
//--------------------------------------------------------------------------
// Copyright (C) Organisation europeenne pour la Recherche nucleaire (CERN)
// All rights reserved.
//
// For the licensing terms see $DD4hepINSTALL/LICENSE.
// For the list of contributors see $DD4hepINSTALL/doc/CREDITS.
//
//  \author Markus Frank
//  \date   2015-11-09
//
//==========================================================================

// Framework include files
#include <DDG4/Geant4DetectorConstruction.h>

/// Namespace for the AIDA detector description toolkit
namespace dd4hep {

  /// Namespace for the Geant4 based simulation part of the AIDA detector description toolkit
  namespace sim {

    /// Class to create Geant4 detector geometry from TGeo representation in memory
    /**
     *  On demand (ie. when calling "Construct") the dd4hep geometry is converted
     *  to Geant4 with all volumes, assemblies, shapes, materials etc.
     *  The actuak work is performed by the Geant4Converter class called by this method.
     *
     *  \author  M.Frank
     *  \version 1.0
     *  \ingroup DD4HEP_SIMULATION
     */
    class Geant4DetectorGeometryConstruction : public Geant4DetectorConstruction   {
      /// Property: Dump geometry hierarchy if not NULL. Flags can steer actions. 
      unsigned long m_dumpHierarchy = 0;
      /// Property: Flag to debug materials during conversion mechanism
      bool m_debugMaterials         = false;
      /// Property: Flag to debug elements during conversion mechanism
      bool m_debugElements          = false;
      /// Property: Flag to debug shapes during conversion mechanism
      bool m_debugShapes            = false;
      /// Property: Flag to debug volumes during conversion mechanism
      bool m_debugVolumes           = false;
      /// Property: Flag to debug placements during conversion mechanism
      bool m_debugPlacements        = false;
      /// Property: Flag to debug reflections during conversion mechanism
      bool m_debugReflections       = false;
      /// Property: Flag to debug regions during conversion mechanism
      bool m_debugRegions           = false;
      /// Property: Flag to debug limit sets during conversion mechanism
      bool m_debugLimits            = false;
      /// Property: Flag to debug regions during conversion mechanism
      bool m_debugSurfaces          = false;

      /// Property: Flag to dump all placements after the conversion procedure
      bool m_printPlacements        = false;
      /// Property: Flag to dump all sensitives after the conversion procedure
      bool m_printSensitives        = false;

      /// Property: Printout level of info object
      int  m_geoInfoPrintLevel;
      /// Property: G4 GDML dump file name (default: empty. If non empty, dump)
      std::string m_dumpGDML;

      /// Write GDML file
      int writeGDML(const char* gdml_output);
      /// Print geant4 volume 
      int printVolumeObj(const char* vol_path, PlacedVolume pv, int flg);
      /// Print volume tree with attributes
      int printVolumeTree(const char* vol_path);
      /// Print volume tree WITHOUT attributes
      int printVolTree(const char* vol_path);
      /// Print geant4 volume tree
      int printG4Tree(const char* vol_path);
      /// Print geant4 volume
      int printVolume(const char* vol_path);
      /// Check geant4 volume
      int checkVolume(const char* vol_path);
      /// Print geant4 material
      int printMaterial(const char* mat_name);

      std::pair<std::string, PlacedVolume> resolve_path(const char* vol_path)   const;
      void printG4(const std::string& prefix, const G4VPhysicalVolume* g4pv)  const;

    public:
      /// Initializing constructor for DDG4
      Geant4DetectorGeometryConstruction(Geant4Context* ctxt, const std::string& nam);
      /// Default destructor
      virtual ~Geant4DetectorGeometryConstruction();
      /// Geometry construction callback. Called at "Construct()"
      void constructGeo(Geant4DetectorConstructionContext* ctxt)  override;
      /// Install command control messenger to write GDML file from command prompt.
      virtual void installCommandMessenger()   override;
    };
  }    // End namespace sim
}      // End namespace dd4hep


// Framework include files
#include <DD4hep/InstanceCount.h>
#include <DD4hep/DetectorTools.h>
#include <DD4hep/DD4hepUnits.h>
#include <DD4hep/Printout.h>
#include <DD4hep/Detector.h>

#include <DDG4/Geant4HierarchyDump.h>
#include <DDG4/Geant4UIMessenger.h>
#include <DDG4/Geant4Converter.h>
#include <DDG4/Geant4Kernel.h>
#include <DDG4/Factories.h>

#include <TGeoScaledShape.h>

// Geant4 include files
#include <G4LogicalVolume.hh>
#include <G4PVPlacement.hh>
#include <G4Material.hh>
#include <G4Version.hh>
#include <G4VSolid.hh>
#include "CLHEP/Units/SystemOfUnits.h"

#ifndef GEANT4_NO_GDML
#include <G4GDMLParser.hh>
#endif

#include <cmath>

using namespace std;
using namespace dd4hep;
using namespace dd4hep::sim;
DECLARE_GEANT4ACTION(Geant4DetectorGeometryConstruction)

/// Initializing constructor for other clients
Geant4DetectorGeometryConstruction::Geant4DetectorGeometryConstruction(Geant4Context* ctxt, const string& nam)
: Geant4DetectorConstruction(ctxt,nam)
{
  declareProperty("DebugMaterials",    m_debugMaterials);
  declareProperty("DebugElements",     m_debugElements);
  declareProperty("DebugShapes",       m_debugShapes);
  declareProperty("DebugVolumes",      m_debugVolumes);
  declareProperty("DebugPlacements",   m_debugPlacements);
  declareProperty("DebugReflections",  m_debugReflections);
  declareProperty("DebugRegions",      m_debugRegions);
  declareProperty("DebugLimits",       m_debugLimits);
  declareProperty("DebugSurfaces",     m_debugSurfaces);

  declareProperty("PrintPlacements",   m_printPlacements);
  declareProperty("PrintSensitives",   m_printSensitives);
  declareProperty("GeoInfoPrintLevel", m_geoInfoPrintLevel = DEBUG);

  declareProperty("DumpHierarchy",     m_dumpHierarchy);
  declareProperty("DumpGDML",          m_dumpGDML="");
  InstanceCount::increment(this);
}

/// Default destructor
Geant4DetectorGeometryConstruction::~Geant4DetectorGeometryConstruction() {
  InstanceCount::decrement(this);
}

/// Geometry construction callback. Called at "Construct()"
void Geant4DetectorGeometryConstruction::constructGeo(Geant4DetectorConstructionContext* ctxt)   {
  Geant4Mapping&  g4map = Geant4Mapping::instance();
  DetElement      world = ctxt->description.world();
  Geant4Converter conv(ctxt->description, outputLevel());
  conv.debugMaterials   = m_debugMaterials;
  conv.debugElements    = m_debugElements;
  conv.debugShapes      = m_debugShapes;
  conv.debugVolumes     = m_debugVolumes;
  conv.debugRegions     = m_debugRegions;
  conv.debugSurfaces    = m_debugSurfaces;
  conv.debugPlacements  = m_debugPlacements;
  conv.debugReflections = m_debugReflections;

  ctxt->geometry = conv.create(world).detach();
  ctxt->geometry->printLevel = outputLevel();
  g4map.attach(ctxt->geometry);
  G4VPhysicalVolume* w = ctxt->geometry->world();
  // Save away the reference to the world volume
  context()->kernel().setWorld(w);
  // Create Geant4 volume manager only if not yet available
  g4map.volumeManager();
  if ( m_dumpHierarchy != 0 )   {
    Geant4HierarchyDump dmp(ctxt->description, m_dumpHierarchy);
    dmp.dump("",w);
  }
  ctxt->world = w;
  if ( !m_dumpGDML.empty() ) writeGDML(m_dumpGDML.c_str());
  else if ( ::getenv("DUMP_GDML") ) writeGDML(::getenv("DUMP_GDML"));
  enableUI();
}

pair<string, PlacedVolume>
Geant4DetectorGeometryConstruction::resolve_path(const char* vol_path)  const {
  string       p   = vol_path;
  Detector&    det = context()->kernel().detectorDescription();
  PlacedVolume top = det.world().placement();
  PlacedVolume pv  = detail::tools::findNode(top, p);
  if ( !pv.isValid() )    {
    DetElement de = detail::tools::findElement(det, p);
    if ( de.isValid() )  {
      pv = de.placement();
      p = detail::tools::placementPath(de);
    }
  }
  return make_pair(p,pv);
}

/// Print geant4 material
int Geant4DetectorGeometryConstruction::printMaterial(const char* mat_name)  {
  if ( mat_name )   {
    auto& g4map = Geant4Mapping::instance().data().g4Materials;
    for ( auto it = g4map.begin(); it != g4map.end(); ++it )  {
      const auto* mat = (*it).second;
      if ( mat->GetName() == mat_name )   {
	stringstream output;
        const auto* ion = mat->GetIonisation();
        printP2("+++  Dump of GEANT4 material: %s", mat_name);
        output << mat;
        if ( ion )   {
          output << "          MEE:  ";
          output << setprecision(12);
          output << ion->GetMeanExcitationEnergy()/CLHEP::eV;
          output << " [eV]";
        }
        else
          output << "          MEE: UNKNOWN";
	always("+++ printMaterial: \n%s\n", output.str().c_str());
        return 1;
      }
    }
    warning("+++ printMaterial: FAILED to find the material %s", mat_name);
  }
  warning("+++ printMaterial: Property materialName not set!");
  return 0;
}

/// Print geant4 volume
int Geant4DetectorGeometryConstruction::printVolumeObj(const char* vol_path, PlacedVolume pv, int flg)   {
  if ( pv.isValid() )   {
    const G4LogicalVolume* vol = 0;
    auto& g4map = Geant4Mapping::instance().data();
    auto pit = g4map.g4Placements.find(pv.ptr());
    auto vit = g4map.g4Volumes.find(pv.volume());
    warning("+++ printVolume: %s", vol_path);
    if ( vit != g4map.g4Volumes.end() )   {
      vol = (*vit).second;
      auto* sol = vol->GetSolid();
      const auto* mat = vol->GetMaterial();
      const auto* ion = mat->GetIonisation();
      Solid sh  = pv.volume().solid();
      if ( flg )  {
	printP2("++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++");
	printP2("+++  Dump of GEANT4 solid: %s", vol_path);
      }
      stringstream output;
      if ( flg )   {
	output << mat;
	if ( ion )   {
	  output << "          MEE:  ";
	  output << setprecision(12);
	  output << ion->GetMeanExcitationEnergy()/CLHEP::eV;
	  output << " [eV]";
	}
	else
	  output << "          MEE: UNKNOWN";
      }
      if ( flg )   {
	output << endl << *sol;
	printP2("%s", output.str().c_str());
	printP2("+++  Dump of ROOT   solid: %s", vol_path);
	sh->InspectShape();
	if ( sh->IsA() == TGeoScaledShape::Class() )    {
	  TGeoScaledShape* scaled = (TGeoScaledShape*)sh.ptr();
	  const Double_t* scale = scaled->GetScale()->GetScale();
	  double dot = scale[0]*scale[1]*scale[2];
	  printP2("+++ TGeoScaledShape: %8.3g  %8.3g  %8.3g  [%s]", scale[0], scale[1], scale[2],
		  dot > 0e0 ? "RIGHT handed" : "LEFT handed");
	}
	else if ( pit != g4map.g4Placements.end() )   {
	  const G4VPhysicalVolume* pl = (*pit).second;
	  const G4RotationMatrix* rot = pl->GetRotation();
	  const G4ThreeVector& tr = pl->GetTranslation();
	  G4Transform3D transform(rot ? *rot : G4RotationMatrix(), tr);
	  HepGeom::Scale3D  sc;
	  HepGeom::Rotate3D rr;
	  G4Translate3D     tt;
	  transform.getDecomposition(sc,rr,tt);
	  double dot = sc(0,0)*sc(1,1)*sc(2,2);
	  printP2("+++ TGeoShape:       %8.3g  %8.3g  %8.3g  [%s]", sc(0,0), sc(1,1), sc(2,2),
		  dot > 0e0 ? "RIGHT handed" : "LEFT handed");
	}
	const TGeoMatrix* matrix = pv->GetMatrix();
	printP2("+++ TGeoMatrix:      %s",
		matrix->TestBit(TGeoMatrix::kGeoReflection) ? "LEFT handed" : "RIGHT handed");        
	printP2("+++ Shape: %s  cubic volume: %8.3g mm^3  area: %8.3g mm^2",
		sol->GetName().c_str(), sol->GetCubicVolume(), sol->GetSurfaceArea());
      }
      return 1;
    }
    else   {
      auto ai = g4map.g4AssemblyVolumes.find(pv.ptr());
      if ( ai != g4map.g4AssemblyVolumes.end() )   {
        Volume v = pv.volume();
        warning("+++ printVolume: volume %s is an assembly...need to resolve imprint",vol_path);
        for(Int_t i=0; i < v->GetNdaughters(); ++i)   {
          TGeoNode*   dau_nod = v->GetNode(i);
          string p = vol_path + string("/") + dau_nod->GetName();
          printVolumeObj(p.c_str(), dau_nod, flg);
        }
        return 0;
      }
    }
    warning("+++ printVolume: FAILED to find the volume %s in geant4 mapping...",vol_path);
    return 0;
  }
  warning("+++ printVolume: FAILED to dump invalid volume",vol_path);
  return 0;
}

/// Print geant4 volume
int Geant4DetectorGeometryConstruction::printVolume(const char* vol_path)  {
  if ( vol_path )   {
    auto physVol = resolve_path(vol_path);
    if ( physVol.second.isValid() )    {
      return printVolumeObj(vol_path, physVol.second, ~0x0);
    }
  }
  warning("+++ printVolume: Property VolumePath not set. [Ignored]");
  return 0;
}

/// Print geant4 volume
int Geant4DetectorGeometryConstruction::printVolumeTree(const char* vol_path)  {
  if ( vol_path )   {
    auto [p, pv] = resolve_path(vol_path);
    if ( pv.isValid() )    {
      if ( printVolumeObj(p.c_str(), pv, ~0x0) )     {
	TGeoVolume* vol = pv->GetVolume();
	for(Int_t i=0; i < vol->GetNdaughters(); ++i)   {
	  PlacedVolume dau_pv(vol->GetNode(i));
	  string path = (p + "/") + dau_pv.name();
	  if ( printVolumeTree(path.c_str()) )     {
	  }
	}
      }
      return 1;
    }
  }
  warning("+++ printVolume: Could not access Volume/DetElement '%s'", vol_path ? vol_path : "UNKNOWN");
  return 0;
}

int Geant4DetectorGeometryConstruction::printVolTree(const char* vol_path)  {
  if ( vol_path )   {
    auto [p, pv] = resolve_path(vol_path);
    if ( pv.isValid() )    {
      if ( printVolumeObj(p.c_str(), pv, 0) )     {
	TGeoVolume* vol = pv->GetVolume();
	for(Int_t i=0; i < vol->GetNdaughters(); ++i)   {
	  PlacedVolume dau_pv(vol->GetNode(i));
	  string path = (p + "/") + dau_pv.name();
	  if ( printVolTree(path.c_str()) )     {
	  }
	}
      }
      return 1;
    }
  }
  warning("+++ printVolume: Could not access Volume/DetElement '%s'", vol_path ? vol_path : "UNKNOWN");
  return 0;
}

/// Check geant4 volume
int Geant4DetectorGeometryConstruction::checkVolume(const char* vol_path)  {
  if ( vol_path )   {
    auto physVol = resolve_path(vol_path);
    auto pv = physVol.second;
    if ( pv.isValid() )   {
      auto& g4map = Geant4Mapping::instance().data().g4Volumes;
      auto it = g4map.find(pv.volume());
      if ( it != g4map.end() )   {
        const G4LogicalVolume* vol = (*it).second;
        auto* g4_sol = vol->GetSolid();
        Box   rt_sol = pv.volume().solid();
        printP2("Geant4 Shape: %s  cubic volume: %8.3g mm^3  area: %8.3g mm^2",
                g4_sol->GetName().c_str(), g4_sol->GetCubicVolume(), g4_sol->GetSurfaceArea());
#if G4VERSION_NUMBER>=1040
        G4ThreeVector pMin, pMax;
        double conv = (dd4hep::centimeter/CLHEP::centimeter)/2.0;
        g4_sol->BoundingLimits(pMin,pMax);
        printP2("Geant4 Bounding box extends:    %8.3g  %8.3g %8.3g",
                (pMax.x()-pMin.x())*conv, (pMax.y()-pMin.y())*conv, (pMax.z()-pMin.z())*conv);
#endif
        printP2("ROOT   Bounding box dimensions: %8.3g  %8.3g %8.3g",
                rt_sol->GetDX(), rt_sol->GetDY(), rt_sol->GetDZ());
        
        return 1;
      }
    }
    warning("+++ checkVolume: FAILED to find the volume %s from the top volume",vol_path);
  }
  warning("+++ checkVolume: Property VolumePath not set. [Ignored]");
  return 0;
}

/// Write GDML file
int Geant4DetectorGeometryConstruction::writeGDML(const char* output)  {
#ifdef GEANT4_NO_GDML
  warning("+++ writeGDML: GDML not found in the present Geant4 build! Output: %s not written", output);
#else
  G4VPhysicalVolume* w  = context()->world();
  if ( output && ::strlen(output) > 0 && output != m_dumpGDML.c_str() )
    m_dumpGDML = output;

  if ( !m_dumpGDML.empty() ) {
    G4GDMLParser parser;
    parser.Write(m_dumpGDML.c_str(), w);
    info("+++ writeGDML: Wrote GDML file: %s", m_dumpGDML.c_str());
    return 1;
  }
  else {
    const char* gdml_dmp = ::getenv("DUMP_GDML");
    if ( gdml_dmp )    {
      G4GDMLParser parser;
      parser.Write(gdml_dmp, w);
      info("+++ writeGDML: Wrote GDML file: %s", gdml_dmp);
      return 1;
    }
  }
  warning("+++ writeGDML: Neither property DumpGDML nor environment DUMP_GDML set. No file written!");
#endif
  return 0;
}

void Geant4DetectorGeometryConstruction::printG4(const string& prefix, const G4VPhysicalVolume* g4pv)    const   {
  string path = prefix + "/";
  printP2(  "+++  GEANT4 volume: %s", prefix.c_str());
  auto* g4v = g4pv->GetLogicalVolume();
  for(size_t i=0, n=g4v->GetNoDaughters(); i<n; ++i)    {
    auto* dau = g4v->GetDaughter(i);
    printG4(path + dau->GetName(), dau);
  }
}

int Geant4DetectorGeometryConstruction::printG4Tree(const char* vol_path)  {
  if ( vol_path )   {
    auto [p, pv] = resolve_path(vol_path);
    if ( pv.isValid() )    {
      auto& g4map = Geant4Mapping::instance().data().g4Placements;
      auto it = g4map.find(pv);
      if ( it != g4map.end() )   {
	printG4(p, (*it).second);
      }
      return 1;
    }
  }
  warning("+++ printVolume: Could not access Volume/DetElement '%s'", vol_path ? vol_path : "UNKNOWN");
  return 0;
}

/// Install command control messenger to write GDML file from command prompt.
void Geant4DetectorGeometryConstruction::installCommandMessenger()   {
  this->Geant4DetectorConstruction::installCommandMessenger();
  m_control->addCall("writeGDML", "GDML: write geometry to file: '"+m_dumpGDML+
                     "' [uses argument - or - property DumpGDML]",
                     Callback(this).make(&Geant4DetectorGeometryConstruction::writeGDML),1);
  m_control->addCall("printVolume", "Print Geant4 volume properties [uses argument]",
                     Callback(this).make(&Geant4DetectorGeometryConstruction::printVolume),1);
  m_control->addCall("printTree",   "Print volume tree WITHOUT properties [uses argument]",
                     Callback(this).make(&Geant4DetectorGeometryConstruction::printVolTree),1);
  m_control->addCall("printG4Tree",   "Print Geant4 volume tree [uses argument]",
                     Callback(this).make(&Geant4DetectorGeometryConstruction::printG4Tree),1);
  m_control->addCall("printVolumeTree", "Print volume tree with properties [uses argument]",
                     Callback(this).make(&Geant4DetectorGeometryConstruction::printVolumeTree),1);
  m_control->addCall("checkVolume", "Check Geant4 volume properties [uses argument]",
                     Callback(this).make(&Geant4DetectorGeometryConstruction::checkVolume),1);
  m_control->addCall("printMaterial", "Print Geant4 material properties [uses argument]",
                     Callback(this).make(&Geant4DetectorGeometryConstruction::printMaterial),1);
}