/**
 * Copyright (C) 2019 Patrik Dufresne Service Logiciel <info@patrikdufresne.com>
 *
 * This program 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 3 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser 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.
 */
package com.planimod.core.planif;

import java.util.AbstractCollection;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.apache.commons.collections.Predicate;
import org.apache.commons.collections.iterators.FilterIterator;

import com.planimod.core.Shift;
import com.planimod.core.Task;
import com.planimod.core.Team;
import com.planimod.core.TimeRanges;

class Table {

    /**
     * Immutable class representing a single Gl.
     * 
     * @author Patrik Dufresne
     */
    private abstract static class Gl {

        public static Table.Gl fromShift(final Shift s, final Date start, final Date end) {
            return new Gl() {

                public Date getEndDate() {
                    if (s.getEndDate().before(end)) {
                        return s.getEndDate();
                    }
                    return end;
                }

                public Shift getShift() {
                    return s;
                }

                public Date getStartDate() {
                    if (s.getStartDate().after(start)) {
                        return s.getStartDate();
                    }
                    return start;
                }
            };
        }

        public static Table.Gl fromTask(final Task t) {
            return new Gl() {

                public Date getEndDate() {
                    return t.getEndDate();
                }

                public Shift getShift() {
                    return t.getProductionEvent().getShift();
                }

                public Date getStartDate() {
                    return t.getStartDate();
                }
            };
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (obj == null) return false;
            if (!(obj instanceof Table.Gl)) return false;
            Table.Gl other = (Table.Gl) obj;
            return getEndDate().compareTo(other.getEndDate()) == 0
                    && getStartDate().compareTo(other.getStartDate()) == 0
                    && getShift().equals(other.getShift());
        }

        public abstract Date getEndDate();

        public abstract Shift getShift();

        public abstract Date getStartDate();

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + getEndDate().hashCode();
            result = prime * result + getShift().hashCode();
            result = prime * result + getStartDate().hashCode();
            return result;
        }

        @Override
        public String toString() {
            return "Gl [getShift()=" + getShift() + ", getStartDate()=" + getStartDate() + ", getEndDate()=" + getEndDate() + "]";
        }
    }

    /**
     * Create a new instance of {@link Table} from the shifts and tasks specified.
     * 
     * @param allShifts
     *            the collection of all shifts.
     * @param tasks
     *            the ordered list of tasks.
     * @return a new instance of Table
     */
    public static Table create(Collection<Shift> allShifts, List<Task> tasks) {
        // Count the maximum number of team
        Set<Team> set = new HashSet<Team>();
        for (Shift s : allShifts) {
            set.add(s.getTeam());
        }
        /*
         * Compute the tables
         */
        List<Table.Gl> glList = new ArrayList<Table.Gl>();
        List<Team> teamList = new ArrayList<Team>();
        boolean[][] tj = new boolean[set.size()][tasks.size()];
        boolean[][] rtj = new boolean[set.size()][tasks.size()];
        List<boolean[]> glTable = new ArrayList<boolean[]>();
        // For each task find the matching team and gl.
        int j, k, l;
        Task task = null;
        for (k = 0; k < tasks.size(); k++) {
            task = tasks.get(k);
            // If the related position is swappable, search for every
            // matching shift, otherwise do it for a single shift.
            for (Shift shift : task.getPosition().getSwappable() ? allShifts : Arrays.asList(task.getProductionEvent().getShift())) {
                // Check if the shift contains the task and vice-versa.
                if (TimeRanges.containsEquals(shift, task.getProductionEvent().getShift())
                        || TimeRanges.containsEquals(task.getProductionEvent().getShift(), shift)) {
                    // Find corresponding team
                    j = teamList.indexOf(shift.getTeam());
                    if (j == -1) {
                        j = teamList.size();
                        teamList.add(shift.getTeam());
                    }
                    tj[j][k] = true;
                    if (shift.equals(task.getProductionEvent().getShift())) {
                        rtj[j][k] = true;
                    }
                    // Sets corresponding gl
                    Table.Gl item = Gl.fromShift(shift, task.getStartDate(), task.getEndDate());
                    l = glList.indexOf(item);
                    if (l == -1) {
                        l = glTable.size();
                        glList.add(item);
                        glTable.add(new boolean[tasks.size()]);
                    }
                    glTable.get(l)[k] = true;
                }
            }
        }
        /*
         * Create a copy of the table by removing the unused row.
         */
        if (tj.length != teamList.size()) {
            tj = Arrays.copyOf(tj, teamList.size());
            rtj = Arrays.copyOf(rtj, teamList.size());
        }
        boolean[][] gl = new boolean[glTable.size()][tasks.size()];
        glTable.toArray(gl);
        return new Table((List<Table.Gl>) glList, teamList, tj, rtj, gl);
    }

    /**
     * The Gl table.
     */
    boolean[][] gl;

    private List<Table.Gl> gls;

    /**
     * Association table the real team, according to task.getShift().getTeam().
     */
    boolean[][] rtj;

    /**
     * 
     */
    private List<Team> teams;

    /**
     * Association table including swappable tasks.
     */
    boolean[][] tj;

    /**
     * Private constructor to create the TjTable.
     * 
     * @param gl
     */
    private Table(List<Table.Gl> gls, List<Team> teams, boolean[][] table, boolean[][] rtj, boolean[][] gl) {
        this.gls = gls;
        this.teams = teams;
        this.tj = table;
        this.rtj = rtj;
        this.gl = gl;
    }

    /**
     * Return the team as the index <code>j</code>.
     * 
     * @param j
     *            the team index
     * @return the team
     */
    public Team getTeam(int j) {
        return this.teams.get(j);
    }

    /**
     * This function return a view on the Gl table. Each entry in this table represent one Gl. An entry is a coefficient
     * list (zero or one) defining if a task is part of the Gl.
     * 
     * @return the Gl table.
     */
    public List<List<Double>> gl() {
        // Create a view on the gl
        return new AbstractList<List<Double>>() {

            @Override
            public List<Double> get(int l) {
                return new CoefficientList(Table.this.gl[l]);
            }

            @Override
            public int size() {
                return Table.this.gl.length;
            }
        };
    }

    /**
     * Return every Gl intersecting with the task <code>k</code> specified. The integer list correspond to a coefficient
     * list for the tasks.
     * 
     * @param k
     *            the task
     * @return a collection of Gl
     */
    public Collection<List<Integer>> glIntersectTask(final int k) {
        // Create an abstract array filtering the Gl not intersecting with
        // k.
        return new AbstractCollection<List<Integer>>() {

            /**
             * This implementation is wrapping the iterator with a FilterIterator.
             */
            @Override
            @SuppressWarnings("unchecked")
            public Iterator<List<Integer>> iterator() {
                return new FilterIterator(gl().iterator(), new Predicate() {

                    @Override
                    public boolean evaluate(Object object) {
                        return SolveRunnable.ONE.equals(((List<Integer>) object).get(k));
                    }
                });
            }

            /**
             * This implementation use the iterator to count the number of entries.
             */
            @Override
            public int size() {
                int count = 0;
                Iterator<List<Integer>> iter = iterator();
                while (iter.hasNext()) {
                    iter.next();
                    count++;
                }
                return count;
            }
        };
    }

    /**
     * Return the index of the team specified.
     * 
     * @param team
     *            the team for which to find the index
     * @return the team index of -1 if not found.
     */
    public int indexOf(Team team) {
        return this.teams.indexOf(team);
    }

    /**
     * Check if the task <code>k</code> is part of team <code>j</code>.
     * 
     * @param k
     *            the task
     * @param j
     *            the team
     * @return True if the task is part of the team as real
     */
    public boolean kInRTj(int k, int j) {
        return this.rtj[j][k];
    }

    /**
     * Check if the task <code>k</code> is part of team <code>j</code>.
     * <p>
     * Same as <code>tj().get(j).get(k).equals(ONE)</code>.
     * 
     * @param k
     *            the task
     * @param j
     *            the team
     * @return True if the task is part of the team.
     */
    public boolean kInTj(int k, int j) {
        return this.tj[j][k];
    }

    /**
     * This function return a view on the RTj table. Each entry in this table represent one team. An entry is a
     * coefficient list (zero or one) defining if the task is part of the team for real (not as a swappable).
     * 
     * @return the RTj table
     */
    public List<List<Double>> rtj() {
        // Create a view on the gl
        return new AbstractList<List<Double>>() {

            @Override
            public List<Double> get(int l) {
                return new CoefficientList(Table.this.rtj[l]);
            }

            @Override
            public int size() {
                return Table.this.rtj.length;
            }
        };
    }

    /**
     * This function return a reversed view on the RTj table.
     * 
     * @return
     */
    public List<List<Double>> rtjReverse() {
        // Create a view on the RTj
        return new AbstractList<List<Double>>() {

            @Override
            public List<Double> get(int l) {
                return new CoefficientList(Table.this.rtj[l], true);
            }

            @Override
            public int size() {
                return Table.this.rtj.length;
            }
        };
    }

    public int teamForGl(int l) {
        Team team = this.gls.get(l).getShift().getTeam();
        return this.teams.indexOf(team);
    }

    /**
     * Return the complete list of teams.
     * 
     * @return
     */
    public Collection<Team> teams() {
        return Collections.unmodifiableList(this.teams);
    }

    /**
     * This function return a view on the Tj table. Each entry in this table represent one team. An entry is a
     * coefficient list (zero or one) defining if the task is part of the team.
     * <p>
     * If a task <code>k</code> is part of the team <code>j</code>, then tj().get(j)[k] equals ONE, otherwise it's null.
     * 
     * @return the Tj table
     */
    public List<List<Double>> tj() {
        // Create a view on the gl
        return new AbstractList<List<Double>>() {

            @Override
            public List<Double> get(int l) {
                return new CoefficientList(Table.this.tj[l]);
            }

            @Override
            public int size() {
                return Table.this.tj.length;
            }
        };
    }
}