/*
	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
	as published by the Free Software Foundation; either version 2
	of the License, or (at your option) any later version.

	This program 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 General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 51 Franklin Street, Fifth Floor,
	Boston, MA  02110-1301, USA.

	---
	Copyright (C) 2011 - 2015, Simon Hampe <simon.hampe@googlemail.com>

	Implements the description of the intersection product in the projective
	torus given by Jensen and Yu.
	*/

#include "polymake/client.h"
#include "polymake/Matrix.h"
#include "polymake/Rational.h"
#include "polymake/Vector.h"
#include "polymake/PowerSet.h"
#include "polymake/Array.h"
#include "polymake/IncidenceMatrix.h"
#include "polymake/linalg.h"
#include "polymake/Smith_normal_form.h"
#include "polymake/RandomGenerators.h"
#include "polymake/tropical/thomog.h"
#include "polymake/tropical/lattice.h"
#include "polymake/tropical/specialcycles.h"
#include "polymake/tropical/solver_def.h"
#include "polymake/tropical/codim_one_with_locality.h"
#include "polymake/tropical/convex_hull_tools.h"
#include "polymake/tropical/star.h"

namespace polymake { namespace tropical { 

   typedef std::pair< Matrix<Rational>, std::vector< Set<int> > > StarResult;

	//Documentation see perl wrapper
	Integer lattice_index(const Matrix<Integer> &lattice_rays) {
		//Compute the Smith Normal form 
		SmithNormalForm<Integer> solution = smith_normal_form(lattice_rays);

      return abs(accumulate( solution.form.diagonal().slice(sequence(0,solution.rank)), operations::mul()));
	}

	///////////////////////////////////////////////////////////////////////////////////////

	/**
	  @brief Computes the Minkowski multiplicity of two fans, i.e. find two cones that add up to full dimension. Then it chooses an interior vector in the difference and finds all cone differences containing it as an interior vector. For all these differences it adds the product of the weights times the lattice index of the sum of the lattices
	  */
	Integer computeFanMultiplicity(const Matrix<Rational> &xrays, const Matrix<Rational> &xlin,
         const std::vector<Set<int> > &xcones, const Vector<Integer> &xweights, const int xdim,
         const Matrix<Rational> &yrays, const Matrix<Rational> &ylin,
         const std::vector<Set<int> > &ycones, const Vector<Integer> &yweights, const int ydim) {
		Integer weight(0);
		solver<Rational> sv;

		// First, we compute all H-representations of xcone - ycone, keeping
		// only full-dimensional ones
      std::vector< Matrix<Rational> > full_dimensional_cones;
      std::vector<int> full_dimensional_xindex; //Keep track of associated x- and ycones
      std::vector<int> full_dimensional_yindex;
		for(auto xc = ensure( xcones, (pm::cons<pm::end_sensitive, pm::indexed>*)0).begin(); !xc.at_end(); ++xc) {
			for(auto yc = ensure( ycones, (pm::cons<pm::end_sensitive, pm::indexed>*)0).begin(); 
               !yc.at_end(); ++yc) {
				auto x_sub_rays = xrays.minor(*xc,All);
				auto y_sub_rays = (- yrays.minor(*yc,All));
				std::pair<Matrix<Rational>, Matrix<Rational> > eqs = 
					sv.enumerate_facets(zero_vector<Rational>() | x_sub_rays / y_sub_rays, 
							zero_vector<Rational>() | (xlin / ylin),true,false);
				if(eqs.second.rows() == 0){
					full_dimensional_cones.push_back(eqs.first);
					full_dimensional_xindex.push_back(xc.index());
					full_dimensional_yindex.push_back(yc.index());
				}
			}
		}

		//If there are no full-dimensional cones, the result is 0
		if(full_dimensional_cones.size() == 0) return weight;

		//Otherwise, we need to compute a generic vector. We compute a 
		// random vector and go through all cones. We add up appropriate
		// weights for those cones containing the point in their interior.
		// If we find one that contains the point in its boundary, we 
		// create another interior point and try again.
		bool point_found;
		UniformlyRandom<Rational> random_gen;
		Vector<Rational> interior_point(xrays.cols()+1);
		do {
			weight = 0;
			copy_range(random_gen.begin(), entire(interior_point));
			interior_point[0] = 1;
			point_found = true;
			//Now go through all full-dimensional cones
         for(auto fullcone = ensure(full_dimensional_cones,(pm::cons<pm::end_sensitive, pm::indexed>*)0).begin();
               !fullcone.at_end(); ++fullcone) {
				//If the cone is the full space, i.e. has no facets, we don't need to check containment
				bool is_interior = true;
				bool is_in_boundary = false;
				if((*fullcone).rows() > 0) {
					const Vector<Rational> eq_check = (*fullcone) * interior_point;
               for(const auto c : eq_check) {
                  if(c == 0) {
                     is_in_boundary = true; break;
                  }
                  if(c < 0) {
                     is_interior = false; break;
                  }
               }//END check for interiorness
					// If its in the boundary of something, try another point.
					if(is_in_boundary) {
						point_found = false; break;
					}
				}
				//If its interior, add the appropriate weight.
				if(is_interior) {
					Integer latticeIndex = lattice_index(
							lattice_basis_of_cone(
								xrays.minor(xcones[full_dimensional_xindex[fullcone.index()]],All),xlin,xdim,false) / 
							lattice_basis_of_cone(
								yrays.minor(ycones[full_dimensional_yindex[fullcone.index()]],All),ylin,ydim,false));
					weight += (xweights[full_dimensional_xindex[fullcone.index()]] * yweights[full_dimensional_yindex[fullcone.index()]] * latticeIndex);
				}

			}//END iterate full-dimensional cones
		} while(!point_found);


		return weight;
	}

	///////////////////////////////////////////////////////////////////////////////////////

	//Documentation see perl wrapper
	template <typename Addition>
	perl::ListReturn intersect_check_transversality(perl::Object X, perl::Object Y, bool ensure_transversality = false) {
		//Extract values
		const int Xcodim = X.give("PROJECTIVE_CODIMENSION");
		const int Xdim   = X.give("PROJECTIVE_DIM");
		const int Ycodim = Y.give("PROJECTIVE_CODIMENSION");
		const int Ydim   = Y.give("PROJECTIVE_DIM");
		const int Xambi  = X.give("PROJECTIVE_AMBIENT_DIM");


		//If the codimensions of the varieties add up to something larger then CMPLX_AMBIENT_DIM, return the 0-cycle 
		if(Xcodim + Ycodim > Xambi) {
			perl::ListReturn zeroResult;
				zeroResult << empty_cycle<Addition>(Xambi);
				zeroResult << false;
				return zeroResult;
		}


		//Extract values
		const Matrix<Rational> &xrays_ref = X.give("VERTICES");
		const Matrix<Rational> xrays = tdehomog(xrays_ref);
		const Matrix<Rational> &xlin_ref = X.give("LINEALITY_SPACE");
		const Matrix<Rational> xlin = tdehomog(xlin_ref);
		const IncidenceMatrix<> xcones = X.give("MAXIMAL_POLYTOPES");
		const Vector<Integer> xweights = X.give("WEIGHTS");
		const int xambdim = X.give("PROJECTIVE_AMBIENT_DIM");

		const Matrix<Rational> &yrays_ref = Y.give("VERTICES");
		const Matrix<Rational> yrays = tdehomog(yrays_ref);
		const Matrix<Rational> &ylin_ref = Y.give("LINEALITY_SPACE");
		const	Matrix<Rational> ylin = tdehomog(ylin_ref);
		const IncidenceMatrix<> ycones = Y.give("MAXIMAL_POLYTOPES");
		const Vector<Integer> yweights = Y.give("WEIGHTS");
		const int yambdim = Y.give("PROJECTIVE_AMBIENT_DIM");

		if(xambdim != yambdim) {
			throw std::runtime_error("Cannot compute intersection product: Cycles live in different spaces.");
		}

		//Compute the expected dimension of the intersection product 
		int k = Xambi - (Xcodim + Ycodim);

		//Compute the intersection complex
		fan_intersection_result f = cdd_fan_intersection(xrays,xlin,xcones,yrays,ylin,ycones);

		//Now we compute the k-skeleton of the intersection complex together with the data of original maximal
		//cones containing these cones
		const Matrix<Rational> &interrays = f.rays;
		const Matrix<Rational> &interlin = f.lineality_space;
		int i_lineality_dim = rank(f.lineality_space);
      std::vector<Set<int> > intercones;
      std::vector<Set<int> > xcontainers;
      std::vector<Set<int> > ycontainers;

		bool is_transversal = true;

		for(auto ic = ensure( rows(f.cones), (pm::cons<pm::end_sensitive, pm::indexed>*)0).begin();
            !ic.at_end(); ++ic) {
			//Check that the cone dimension is at least the expected dimension
			int cone_dim = rank(interrays.minor(*ic,All)) + i_lineality_dim -1;
			if(cone_dim >= k) {
				if(cone_dim > k) {
					is_transversal = false;
					if(ensure_transversality) { 
						perl::ListReturn zeroResult;
						zeroResult << empty_cycle<Addition>(Xambi);
						zeroResult << false;
						return zeroResult;
					}
				}
				//Now we compute the k-skeleton of the intersection cone
				Vector<Set<int> > singlecone(1); singlecone[0] = *ic;
				IncidenceMatrix<> k_skeleton_matrix(singlecone);
				for(int i = cone_dim; i > k; i--) {
					k_skeleton_matrix = 
						calculateCodimOneData(interrays, k_skeleton_matrix, interlin, IncidenceMatrix<>()).codimOneCones;
				}

				//Go through all cones and add them (if they haven't already been added)
				for(auto kc = entire(rows(k_skeleton_matrix)); !kc.at_end(); ++kc) {
					int cone_index = -1;
					for(auto oc = ensure(intercones, (pm::cons<pm::end_sensitive, pm::indexed>*)0).begin();
                     !oc.at_end(); ++oc) {
						//Since both cones have the same dimension, it suffices to check, whether the old cone
						//is contained in the new cone
						if( incl( *oc, *kc) <= 0) {
							cone_index = oc.index(); break;
						}
					}
					//If it doesn't exist yet, add it
					if(cone_index == -1) {
						intercones.push_back(*kc);
						xcontainers.push_back(Set<int>());
						ycontainers.push_back(Set<int>());
						cone_index = intercones.size()-1;
					}

					//Now add containers
					xcontainers[cone_index] += f.xcontainers.row(ic.index());
					ycontainers[cone_index] += f.ycontainers.row(ic.index());

				}//END iterate all k-skeleton cones

			}//END if cone_dim >= k
		}//END iterate intersection cones

		//If no cones remain, return the zero cycle
		if(intercones.size() == 0) {
			perl::ListReturn zeroResult;
			zeroResult << empty_cycle<Addition>(Xambi);
			zeroResult << false;
			return zeroResult;
		}


		//Now we compute weights
		Vector<Integer> weights(intercones.size());
		Set<int> weight_zero_cones;

		Matrix<Rational> xlin_dehom = xlin.minor(All,~scalar2set(0));
		Matrix<Rational> ylin_dehom = ylin.minor(All,~scalar2set(0));

		for(auto c = ensure(intercones, (pm::cons<pm::end_sensitive, pm::indexed>*)0).begin();
            !c.at_end(); ++c) {
			//Find interior point
			Vector<Rational> interior_point = accumulate(rows(interrays.minor(*c,All)),operations::add());
			Rational count_vertices = accumulate(interrays.col(0).slice(*c),operations::add());
			if(count_vertices != 0) interior_point /= count_vertices;


			//Compute stars
			Matrix<Rational> xstar_rays, ystar_rays;
         std::vector<Set<int>> xstar_cones, ystar_cones;


			StarResult starx = computeStar(interior_point, xrays, xcones.minor(xcontainers[c.index()],All));
			StarResult stary = computeStar(interior_point, yrays, ycones.minor(ycontainers[c.index()],All));
         xstar_rays = starx.first; xstar_cones = starx.second;
         ystar_rays = stary.first; ystar_cones = stary.second;



			Integer w = computeFanMultiplicity(
					xstar_rays, xlin_dehom, xstar_cones, xweights.slice(xcontainers[c.index()]), Xdim,
					ystar_rays, ylin_dehom, ystar_cones, yweights.slice(ycontainers[c.index()]), Ydim);


			weights[c.index()] = w;
			if(w == 0) weight_zero_cones += c.index();

		}

		//Check if any cones remain
		if (weight_zero_cones.size() == int(intercones.size())) {
			perl::ListReturn zeroResult;
			zeroResult << empty_cycle<Addition>(Xambi);
			zeroResult << false;
			return zeroResult;
		}


		//Clean up rays and cones

		IncidenceMatrix<> intercones_matrix(intercones);
		intercones_matrix = intercones_matrix.minor(~weight_zero_cones,All);
		Set<int> used_rays = accumulate(rows(intercones_matrix), operations::add());
		intercones_matrix = intercones_matrix.minor(All,used_rays);
		weights = weights.slice(~weight_zero_cones);

		//Finally create the result  
		perl::Object result(perl::ObjectType::construct<Addition>("Cycle"));
		result.take("VERTICES") << thomog(interrays.minor(used_rays,All));
		result.take("MAXIMAL_POLYTOPES") << intercones_matrix;
		result.take("LINEALITY_SPACE") << thomog(interlin);
		result.take("WEIGHTS") << weights;

		//     return result;
		perl::ListReturn positiveResult;
		positiveResult << result;
		positiveResult << is_transversal;
		return positiveResult;
	}



	// ------------------------- PERL WRAPPERS ---------------------------------------------------

	UserFunction4perl("# @category Lattices"
			"# This computes the index of a lattice in its saturation."
			"# @param Matrix<Integer> m A list of (row) generators of the lattice."
			"# @return Integer The index of the lattice in its saturation.",
			&lattice_index,"lattice_index(Matrix<Integer>)");

	UserFunctionTemplate4perl("# @category Intersection theory"
			"# Computes the intersection product of two tropical cycles in R^n and tests whether the intersection is transversal (in the sense that the cycles intersect set-theoretically in the right dimension)."
			"# @param Cycle X A tropical cycle"
			"# @param Cycle Y A tropical cycle, living in the same space as X"
			"# @param Bool ensure_transversality Whether non-transversal intersections should not be computed. Optional and false by default. If true,"
			"# returns the zero cycle if it detects a non-transversal intersection"
			"# @return List( Cycle intersection product, Bool is_transversal)."
                        "#  Intersection product is a zero cycle if ensure_transversality is true and the intersection is not transversal."
			"#  //is_transversal// is false if the codimensions of the varieties add up to more than the ambient dimension.",
			"intersect_check_transversality<Addition>(Cycle<Addition>,Cycle<Addition>; $=0)");

	InsertEmbeddedRule("# @category Intersection theory"
			"# Computes the intersection product of two tropical cycles in the projective torus"
			"# Use [[intersect_check_transversality]] to check for transversal intersections"
			"# @param Cycle X A tropical cycle"
			"# @param Cycle Y A tropical cycle, living in the same ambient space as X"
			"# @return Cycle The intersection product\n"
			"user_function intersect<Addition>(Cycle<Addition>,Cycle<Addition>) {\n"
			"	my ($X,$Y) = @_;\n"
			"	my @r = intersect_check_transversality($X,$Y);\n"
			"	return $r[0];\n"
			"}\n");

   FunctionTemplate4perl("computeStar(Vector,Matrix,IncidenceMatrix)");

}}
