/*
    Scan Tailor - Interactive post-processing tool for scanned pages.
    Copyright (C)  Joseph Artsimovich <joseph.artsimovich@gmail.com>

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

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "PageLayout.h"
#include "LegacySupport.h"
#include "NumericTraits.h"
#include "XmlMarshaller.h"
#include "XmlUnmarshaller.h"
#include "ToLineProjector.h"
#include "imageproc/PolygonUtils.h"
#include <QPolygonF>
#include <QSizeF>
#include <QPointF>
#include <QTransform>
#include <QDomElement>
#include <QDomDocument>
#include <algorithm>
#include <math.h>
#include <assert.h>

using namespace imageproc;

namespace page_split
{

PageLayout::PageLayout()
    :   m_type(SINGLE_PAGE_UNCUT)
{
}

PageLayout::PageLayout(QRectF const& full_rect)
    :   m_uncutOutline(full_rect),
        m_cutter1(full_rect.topLeft(), full_rect.bottomLeft()),
        m_cutter2(full_rect.topRight(), full_rect.bottomRight()),
        m_type(SINGLE_PAGE_UNCUT)
{
}

PageLayout::PageLayout(QRectF const& full_rect, QLineF const& cutter1, QLineF const& cutter2)
    :   m_uncutOutline(full_rect),
        m_cutter1(cutter1),
        m_cutter2(cutter2),
        m_type(SINGLE_PAGE_CUT)
{
}

PageLayout::PageLayout(QRectF const full_rect, QLineF const& split_line)
    :   m_uncutOutline(full_rect),
        m_cutter1(split_line),
        m_type(TWO_PAGES)
{
}

PageLayout::PageLayout(
    QPolygonF const& outline, QLineF const& cutter1,
    QLineF const& cutter2, Type type)
    :   m_uncutOutline(outline),
        m_cutter1(cutter1),
        m_cutter2(cutter2),
        m_type(type)
{
}

PageLayout::PageLayout(QDomElement const& layout_el)
    :   m_uncutOutline(
            XmlUnmarshaller::polygonF(
                layout_el.namedItem("outline").toElement()
            )
        ),
        m_cutter1(
            XmlUnmarshaller::lineF(
                layout_el.namedItem("cutter1").toElement()
            )
        ),
        m_cutter2(
            XmlUnmarshaller::lineF(
                layout_el.namedItem("cutter2").toElement()
            )
        )
{
    QString const type(layout_el.attribute("type"));
    QDomElement const split_line_el(layout_el.namedItem("split-line").toElement());
    if (split_line_el.isNull()) {
        m_type = typeFromString(type);
    } else {
        // Backwards compatibility with versions < 0.9.9
        // Note that we fill in m_uncutOutline elsewhere, namely in Task::process().
        QLineF const split_line(XmlUnmarshaller::lineF(split_line_el));

        // Backwards compatibility with versions <= 0.9.1
        bool const left_page = (
                                   layout_el.attribute("leftPageValid") == "1"
                               );
        bool const right_page = (
                                    layout_el.attribute("rightPageValid") == "1"
                                );

        if (type == "two-pages" || (left_page && right_page)) {
            m_type = TWO_PAGES;
            m_cutter1 = split_line;
            m_cutter2 = QLineF();
        } else if (type == "left-page" || left_page) {
            m_type = SINGLE_PAGE_CUT;
            m_cutter1 = QLineF(0, 0, 0, 1); // A bit of a hack, but should work.
            m_cutter2 = split_line;
        } else if (type == "right-page" || right_page) {
            m_type = SINGLE_PAGE_CUT;
            m_cutter1 = split_line;
            m_cutter2 = QLineF();
            // This one is a special case.  We don't know the outline at this point,
            // so we make m_cutter2 null.  We'll assign the correct value to it in
            // setUncutOutline().
        } else {
            m_type = SINGLE_PAGE_UNCUT;
            m_cutter1 = QLineF();
            m_cutter2 = QLineF();
        }
    }
}

void
PageLayout::setType(Type type)
{
    m_type = type;
    if (type == TWO_PAGES) {
        m_cutter2 = m_cutter1;
    }
}

void
PageLayout::setUncutOutline(QRectF const& outline)
{
    m_uncutOutline = outline;
    if (m_uncutOutline.size() < 4) {
        // Empty rect?
        return;
    }

    if (m_type == SINGLE_PAGE_CUT && m_cutter2.isNull()) {
        // Backwards compatibility.  See PageLayout(QDomElement const&);
        m_cutter2.setP1(m_uncutOutline[1]);
        m_cutter2.setP2(m_uncutOutline[2]);
    }
}

QLineF const&
PageLayout::cutterLine(int idx) const
{
    assert(idx >= 0 && idx < numCutters());
    return idx == 0 ? m_cutter1 : m_cutter2;
}

void
PageLayout::setCutterLine(int idx, QLineF const& cutter)
{
    assert(idx >= 0 && idx < numCutters());
    (idx == 0 ? m_cutter1 : m_cutter2) = cutter;
}

LayoutType
PageLayout::toLayoutType() const
{
    switch (m_type) {
    case SINGLE_PAGE_UNCUT:
        return page_split::SINGLE_PAGE_UNCUT;
    case SINGLE_PAGE_CUT:
        return page_split::PAGE_PLUS_OFFCUT;
    case TWO_PAGES:
        return page_split::TWO_PAGES;
    }

    assert(!"Unreachable");
    return page_split::SINGLE_PAGE_UNCUT;
}

int
PageLayout::numCutters() const
{
    switch (m_type) {
    case SINGLE_PAGE_UNCUT:
        return 0;
    case SINGLE_PAGE_CUT:
        return 2;
    case TWO_PAGES:
        return 1;
    }

    assert(!"Unreachable");
    return 0;
}

int
PageLayout::numSubPages() const
{
    return m_type == TWO_PAGES ? 2 : 1;
}

QLineF
PageLayout::inscribedCutterLine(int idx) const
{
    assert(idx >= 0 && idx < numCutters());

    if (m_uncutOutline.size() < 4) {
        return QLineF();
    }

    QLineF const raw_line(cutterLine(idx));
    QPointF const origin(raw_line.p1());
    QPointF const line_vec(raw_line.p2() - origin);

    double min_proj = NumericTraits<double>::max();
    double max_proj = NumericTraits<double>::min();
    QPointF min_pt;
    QPointF max_pt;

    QPointF p1, p2;
    double projection;

    for (int i = 0; i < 4; ++i) {
        QLineF const poly_segment(m_uncutOutline[i], m_uncutOutline[(i + 1) & 3]);
        QPointF intersection;
        if (QLineIntersect(poly_segment, raw_line, &intersection) == QLineF::NoIntersection) {
            continue;
        }

        // Project the intersection point on poly_segment to check
        // if it's between its endpoints.
        p1 = poly_segment.p2() - poly_segment.p1();
        p2 = intersection - poly_segment.p1();
        projection = p1.x() * p2.x() + p1.y() * p2.y();
        if (projection < 0 || projection > p1.x() * p1.x() + p1.y() * p1.y()) {
            // Intersection point not on the polygon segment.
            continue;
        }

        // Now project it on raw_line and update min/max projections.
        p1 = intersection - origin;
        p2 = line_vec;
        projection = p1.x() * p2.x() + p1.y() * p2.y();
        if (projection <= min_proj) {
            min_proj = projection;
            min_pt = intersection;
        }
        if (projection >= max_proj) {
            max_proj = projection;
            max_pt = intersection;
        }
    }

    QLineF res(min_pt, max_pt);
    ensureSameDirection(raw_line, res);
    return res;
}

QPolygonF
PageLayout::singlePageOutline() const
{
    if (m_uncutOutline.size() < 4) {
        return QPolygonF();
    }

    switch (m_type) {
    case SINGLE_PAGE_UNCUT:
        return m_uncutOutline;
    case SINGLE_PAGE_CUT:
        break;
    case TWO_PAGES:
        return QPolygonF();
    }

    QLineF const line1(extendToCover(m_cutter1, m_uncutOutline));
    QLineF line2(extendToCover(m_cutter2, m_uncutOutline));
    ensureSameDirection(line1, line2);
    QLineF const reverse_line1(line1.p2(), line1.p1());
    QLineF const reverse_line2(line2.p2(), line2.p1());

    QPolygonF poly;
    poly << line1.p1();
    maybeAddIntersectionPoint(poly, line1.normalVector(), line2.normalVector());
    poly << line2.p1() << line2.p2();
    maybeAddIntersectionPoint(poly, reverse_line1.normalVector(), reverse_line2.normalVector());
    poly << line1.p2();

    return PolygonUtils::round(m_uncutOutline).intersected(PolygonUtils::round(poly));
}

QPolygonF
PageLayout::leftPageOutline() const
{
    if (m_uncutOutline.size() < 4) {
        return QPolygonF();
    }

    switch (m_type) {
    case SINGLE_PAGE_UNCUT:
    case SINGLE_PAGE_CUT:
        return QPolygonF();
    case TWO_PAGES:
        break;
    }

    QLineF const line1(m_uncutOutline[0], m_uncutOutline[3]);
    QLineF line2(extendToCover(m_cutter1, m_uncutOutline));
    ensureSameDirection(line1, line2);
    QLineF const reverse_line1(line1.p2(), line1.p1());
    QLineF const reverse_line2(line2.p2(), line2.p1());

    QPolygonF poly;
    poly << line1.p1();
    maybeAddIntersectionPoint(poly, line1.normalVector(), line2.normalVector());
    poly << line2.p1() << line2.p2();
    maybeAddIntersectionPoint(poly, reverse_line1.normalVector(), reverse_line2.normalVector());
    poly << line1.p2();

    return PolygonUtils::round(m_uncutOutline).intersected(PolygonUtils::round(poly));
}

QPolygonF
PageLayout::rightPageOutline() const
{
    if (m_uncutOutline.size() < 4) {
        return QPolygonF();
    }

    switch (m_type) {
    case SINGLE_PAGE_UNCUT:
    case SINGLE_PAGE_CUT:
        return QPolygonF();
    case TWO_PAGES:
        break;
    }

    QLineF const line1(m_uncutOutline[1], m_uncutOutline[2]);
    QLineF line2(extendToCover(m_cutter1, m_uncutOutline));
    ensureSameDirection(line1, line2);
    QLineF const reverse_line1(line1.p2(), line1.p1());
    QLineF const reverse_line2(line2.p2(), line2.p1());

    QPolygonF poly;
    poly << line1.p1();
    maybeAddIntersectionPoint(poly, line1.normalVector(), line2.normalVector());
    poly << line2.p1() << line2.p2();
    maybeAddIntersectionPoint(poly, reverse_line1.normalVector(), reverse_line2.normalVector());
    poly << line1.p2();

    return PolygonUtils::round(m_uncutOutline).intersected(PolygonUtils::round(poly));
}

QPolygonF
PageLayout::pageOutline(PageId::SubPage const page) const
{
    switch (page) {
    case PageId::SINGLE_PAGE:
        return singlePageOutline();
    case PageId::LEFT_PAGE:
        return leftPageOutline();
    case PageId::RIGHT_PAGE:
        return rightPageOutline();
    }

    assert(!"Unreachable");
    return QPolygonF();
}

PageLayout
PageLayout::transformed(QTransform const& xform) const
{
    return PageLayout(
               xform.map(m_uncutOutline),
               xform.map(m_cutter1), xform.map(m_cutter2), m_type
           );
}

QDomElement
PageLayout::toXml(QDomDocument& doc, QString const& name) const
{
    XmlMarshaller marshaller(doc);

    QDomElement el(doc.createElement(name));
    el.setAttribute("type", typeToString(m_type));
    el.appendChild(marshaller.polygonF(m_uncutOutline, "outline"));

    int const num_cutters = numCutters();
    if (num_cutters > 0) {
        el.appendChild(marshaller.lineF(m_cutter1, "cutter1"));
        if (num_cutters > 1) {
            el.appendChild(marshaller.lineF(m_cutter2, "cutter2"));
        }
    }

    return el;
}

PageLayout::Type
PageLayout::typeFromString(QString const& str)
{
    if (str == "two-pages") {
        return TWO_PAGES;
    } else if (str == "single-cut") {
        return SINGLE_PAGE_CUT;
    } else { // "single-uncut"
        return SINGLE_PAGE_UNCUT;
    }
}

QString
PageLayout::typeToString(Type const type)
{
    char const* str = 0;
    switch (type) {
    case SINGLE_PAGE_UNCUT:
        str = "single-uncut";
        break;
    case SINGLE_PAGE_CUT:
        str = "single-cut";
        break;
    case TWO_PAGES:
        str = "two-pages";
        break;
    }

    return QLatin1String(str);
}

/**
 * Extends or shrinks a line segment in such a way that if you draw perpendicular
 * lines through its endpoints, the given polygon would be squeezed between these
 * two perpendiculars.  This ensures that the resulting line segment intersects
 * all the polygon edges it can possibly intersect.
 */
QLineF
PageLayout::extendToCover(QLineF const& line, QPolygonF const& poly)
{
    if (poly.isEmpty()) {
        return line;
    }

    // Project every vertex of the polygon onto the line and take extremas.

    double min = NumericTraits<double>::max();
    double max = NumericTraits<double>::min();
    ToLineProjector const projector(line);

    for (QPointF const& pt : poly) {
        double const scalar = projector.projectionScalar(pt);
        if (scalar < min) {
            min = scalar;
        }
        if (scalar > max) {
            max = scalar;
        }
    }

    return QLineF(line.pointAt(min), line.pointAt(max));
}

/**
 * Flips \p line2 if that would make the angle between the two lines more acute.
 * The angle between lines is interpreted as an angle between vectors
 * (line1.p2() - line1.p1()) and (line2.p2() - line2.p1()).
 */
void
PageLayout::ensureSameDirection(QLineF const& line1, QLineF& line2)
{
    QPointF const v1(line1.p2() - line1.p1());
    QPointF const v2(line2.p2() - line2.p1());
    double const dot = v1.x() * v2.x() + v1.y() * v2.y();
    if (dot < 0.0) {
        line2 = QLineF(line2.p2(), line2.p1());
    }
}

/**
 * Add the intersection point between \p line1 and \p line2
 * to \p poly, provided they intersect at all and the intersection
 * point is "between" line1.p1() and line2.p1().  We consider a point
 * to be between two other points by projecting it to the line between
 * those two points and checking if the projected point is between them.
 * When finding the intersection point, we treat \p line1 and \p line2
 * as lines, not line segments.
 */
void
PageLayout::maybeAddIntersectionPoint(
    QPolygonF& poly, QLineF const& line1, QLineF const& line2)
{
    QPointF intersection;
    if (QLineIntersect(line1, line2, &intersection) == QLineF::NoIntersection) {
        return;
    }

    ToLineProjector const projector(QLineF(line1.p1(), line2.p1()));
    double const p = projector.projectionScalar(intersection);
    if (p > 0.0 && p < 1.0) {
        poly << intersection;
    }
}

} // namespace page_split
