#include <sys/time.h>
#include <algorithm>

#include "libmythbase/mythconfig.h"

extern "C" {
#include "libavcodec/avcodec.h"        /* AVFrame */
}

// MythTV
#include "libmythbase/compat.h"
#include "libmythbase/mythchrono.h"
#include "libmythbase/mythcorecontext.h"    /* gContext */

// Commercial Flagging headers
#include "BorderDetector.h"
#include "CommDetector2.h"
#include "FrameAnalyzer.h"
#include "TemplateFinder.h"

using namespace frameAnalyzer;
using namespace commDetector2;

BorderDetector::BorderDetector(void)
{
    m_debugLevel = gCoreContext->GetNumSetting("BorderDetectorDebugLevel", 0);

    if (m_debugLevel >= 1)
        LOG(VB_COMMFLAG, LOG_INFO,
            QString("BorderDetector debugLevel %1").arg(m_debugLevel));
}

int
BorderDetector::MythPlayerInited([[maybe_unused]] const MythPlayer *player)
{
    m_timeReported = false;
    memset(&m_analyzeTime, 0, sizeof(m_analyzeTime));
    return 0;
}

void
BorderDetector::setLogoState(TemplateFinder *finder)
{
    m_logoFinder = finder;
    if (m_logoFinder == nullptr)
        return;
    m_logo = m_logoFinder->getTemplate(&m_logoRow, &m_logoCol,
                                       &m_logoWidth, &m_logoHeight);
    if (m_logo == nullptr)
        return;
    LOG(VB_COMMFLAG, LOG_INFO,
        QString("BorderDetector::setLogoState: %1x%2@(%3,%4)")
        .arg(m_logoWidth).arg(m_logoHeight).arg(m_logoCol).arg(m_logoRow));
}

int
BorderDetector::getDimensions(const AVFrame *pgm, int pgmheight,
        long long _frameno, int *prow, int *pcol, int *pwidth, int *pheight)
{
    /*
     * The basic algorithm is to look for pixels of the same color along all
     * four borders of the frame, working inwards until the pixels cease to be
     * of uniform color. This way, letterboxing/pillarboxing bars can be of any
     * color (varying shades of black-grey).
     *
     * If there is a logo, exclude its area from border detection.
     *
     * Return 0 for normal frames; non-zero for monochromatic frames.
     */

    /*
     * TUNABLES:
     *
     * Higher values mean more tolerance for noise (e.g., analog recordings).
     * However, in the absence of noise, content/logos can be cropped away from
     * analysis.
     *
     * Lower values mean less tolerance for noise. In a noisy recording, the
     * transition between pillarbox/letterbox black to content color will be
     * detected as an edge, and thwart logo edge detection. In the absence of
     * noise, content/logos will be more faithfully analyzed.
     */

    /*
     * TUNABLE: The maximum range of values allowed for
     * letterboxing/pillarboxing bars. Usually the bars are black (0x00), but
     * sometimes they are grey (0x71). Sometimes the letterboxing and
     * pillarboxing (when one is embedded inside the other) are different
     * colors.
     */
    static constexpr unsigned char kMaxRange = 32;

    /*
     * TUNABLE: The maximum number of consecutive rows or columns with too many
     * outlier points that may be scanned before declaring the existence of a
     * border.
     */
    static constexpr int    kMaxLines = 2;

    const int               pgmwidth = pgm->linesize[0];

    /*
     * TUNABLE: The maximum number of outlier points in a single row or column
     * with grey values outside of MAXRANGE before declaring the existence of a
     * border.
     */
    const int               MAXOUTLIERS = pgmwidth * 12 / 1000;

    /*
     * TUNABLE: Margins to avoid noise at the extreme edges of the signal
     * (VBI?). (Really, just a special case of VERTSLOP and HORIZSLOP, below.)
     */
    const int               VERTMARGIN = std::max(2, pgmheight * 1 / 60);
    const int               HORIZMARGIN = std::max(2, pgmwidth * 1 / 80);

    /*
     * TUNABLE: Slop to accommodate any jagged letterboxing/pillarboxing edges,
     * or noise between edges and content. (Really, a more general case of
     * VERTMARGIN and HORIZMARGIN, above.)
     */
    const int               VERTSLOP = std::max(kMaxLines, pgmheight * 1 / 120);
    const int               HORIZSLOP = std::max(kMaxLines, pgmwidth * 1 / 160);

    int minrow    = VERTMARGIN;
    int mincol    = HORIZMARGIN;
    int maxrow1   = pgmheight - VERTMARGIN;   /* maxrow + 1 */
    int maxcol1   = pgmwidth - HORIZMARGIN;   /* maxcol + 1 */
    int newrow    = minrow - 1;
    int newcol    = mincol - 1;
    int newwidth  = maxcol1 + 1 - mincol;
    int newheight = maxrow1 + 1 - minrow;
    bool top    = false;
    bool bottom = false;

    auto start = nowAsDuration<std::chrono::microseconds>();

    if (_frameno != kUncached && _frameno == m_frameNo)
        goto done;

    for (;;)
    {
        /* Find left edge. */
        bool left = false;
        uchar minval = UCHAR_MAX;
        uchar maxval = 0;
        int lines = 0;
        int saved = mincol;
        for (int cc = mincol; cc < maxcol1; cc++)
        {
            int outliers = 0;
            bool inrange = true;
            for (int rr = minrow; rr < maxrow1; rr++)
            {
                if (m_logo && rrccinrect(rr, cc, m_logoRow, m_logoCol,
                            m_logoWidth, m_logoHeight))
                    continue;   /* Exclude logo area from analysis. */

                uchar val = pgm->data[0][(rr * pgmwidth) + cc];
                int range = std::max(maxval, val) - std::min(minval, val) + 1;
                if (range > kMaxRange)
                {
                    if (outliers++ < MAXOUTLIERS)
                        continue;   /* Next row. */
                    inrange = false;
                    if (lines++ < kMaxLines)
                        break;  /* Next column. */
                    goto found_left;
                }
                minval = std::min(val, minval);
                maxval = std::max(val, maxval);
            }
            if (inrange)
            {
                saved = cc;
                lines = 0;
            }
        }
found_left:
        if (newcol != saved + 1 + HORIZSLOP)
        {
            newcol = std::min(maxcol1, saved + 1 + HORIZSLOP);
            newwidth = std::max(0, maxcol1 - newcol);
            left = true;
        }

        if (!newwidth)
            goto monochromatic_frame;

        mincol = newcol;

        /*
         * Find right edge. Keep same minval/maxval (pillarboxing colors) as
         * left edge.
         */
        bool right = false;
        lines = 0;
        saved = maxcol1 - 1;
        for (int cc = maxcol1 - 1; cc >= mincol; cc--)
        {
            int outliers = 0;
            bool inrange = true;
            for (int rr = minrow; rr < maxrow1; rr++)
            {
                if (m_logo && rrccinrect(rr, cc, m_logoRow, m_logoCol,
                            m_logoWidth, m_logoHeight))
                    continue;   /* Exclude logo area from analysis. */

                uchar val = pgm->data[0][(rr * pgmwidth) + cc];
                int range = std::max(maxval, val) - std::min(minval, val) + 1;
                if (range > kMaxRange)
                {
                    if (outliers++ < MAXOUTLIERS)
                        continue;   /* Next row. */
                    inrange = false;
                    if (lines++ < kMaxLines)
                        break;  /* Next column. */
                    goto found_right;
                }
                minval = std::min(val, minval);
                maxval = std::max(val, maxval);
            }
            if (inrange)
            {
                saved = cc;
                lines = 0;
            }
        }
found_right:
        if (newwidth != saved - mincol - HORIZSLOP)
        {
            newwidth = std::max(0, saved - mincol - HORIZSLOP);
            right = true;
        }

        if (!newwidth)
            goto monochromatic_frame;

        if (top || bottom)
            break;  /* Do not repeat letterboxing check. */

        maxcol1 = mincol + newwidth;

        /* Find top edge. */
        top = false;
        minval = UCHAR_MAX;
        maxval = 0;
        lines = 0;
        saved = minrow;
        for (int rr = minrow; rr < maxrow1; rr++)
        {
            int outliers = 0;
            bool inrange = true;
            for (int cc = mincol; cc < maxcol1; cc++)
            {
                if (m_logo && rrccinrect(rr, cc, m_logoRow, m_logoCol,
                            m_logoWidth, m_logoHeight))
                    continue;   /* Exclude logo area from analysis. */

                uchar val = pgm->data[0][(rr * pgmwidth) + cc];
                int range = std::max(maxval, val) - std::min(minval, val) + 1;
                if (range > kMaxRange)
                {
                    if (outliers++ < MAXOUTLIERS)
                        continue;   /* Next column. */
                    inrange = false;
                    if (lines++ < kMaxLines)
                        break;  /* Next row. */
                    goto found_top;
                }
                minval = std::min(val, minval);
                maxval = std::max(val, maxval);
            }
            if (inrange)
            {
                saved = rr;
                lines = 0;
            }
        }
found_top:
        if (newrow != saved + 1 + VERTSLOP)
        {
            newrow = std::min(maxrow1, saved + 1 + VERTSLOP);
            newheight = std::max(0, maxrow1 - newrow);
            top = true;
        }

        if (!newheight)
            goto monochromatic_frame;

        minrow = newrow;

        /* Find bottom edge. Keep same minval/maxval as top edge. */
        bottom = false;
        lines = 0;
        saved = maxrow1 - 1;
        for (int rr = maxrow1 - 1; rr >= minrow; rr--)
        {
            int outliers = 0;
            bool inrange = true;
            for (int cc = mincol; cc < maxcol1; cc++)
            {
                if (m_logo && rrccinrect(rr, cc, m_logoRow, m_logoCol,
                            m_logoWidth, m_logoHeight))
                    continue;   /* Exclude logo area from analysis. */

                uchar val = pgm->data[0][(rr * pgmwidth) + cc];
                int range = std::max(maxval, val) - std::min(minval, val) + 1;
                if (range > kMaxRange)
                {
                    if (outliers++ < MAXOUTLIERS)
                        continue;   /* Next column. */
                    inrange = false;
                    if (lines++ < kMaxLines)
                        break;  /* Next row. */
                    goto found_bottom;
                }
                minval = std::min(val, minval);
                maxval = std::max(val, maxval);
            }
            if (inrange)
            {
                saved = rr;
                lines = 0;
            }
        }
found_bottom:
        if (newheight != saved - minrow - VERTSLOP)
        {
            newheight = std::max(0, saved - minrow - VERTSLOP);
            bottom = true;
        }

        if (!newheight)
            goto monochromatic_frame;

        if (left || right)
            break;  /* Do not repeat pillarboxing check. */

        maxrow1 = minrow + newheight;
    }

    m_frameNo = _frameno;
    m_row = newrow;
    m_col = newcol;
    m_width = newwidth;
    m_height = newheight;
    m_isMonochromatic = false;
    goto done;

monochromatic_frame:
    m_frameNo = _frameno;
    m_row = newrow;
    m_col = newcol;
    m_width = newwidth;
    m_height = newheight;
    m_isMonochromatic = true;

done:
    *prow = m_row;
    *pcol = m_col;
    *pwidth = m_width;
    *pheight = m_height;

    auto end = nowAsDuration<std::chrono::microseconds>();
    m_analyzeTime += (end - start);

    return m_isMonochromatic ? -1 : 0;
}

int
BorderDetector::reportTime(void)
{
    if (!m_timeReported)
    {
        LOG(VB_COMMFLAG, LOG_INFO, QString("BD Time: analyze=%1s")
                .arg(strftimeval(m_analyzeTime)));
        m_timeReported = true;
    }
    return 0;
}

/* vim: set expandtab tabstop=4 shiftwidth=4: */
