/*
 * This file is part of bino, a 3D video player.
 *
 * Copyright (C) 2010
 * Martin Lambers <marlam@marlam.de>
 * Frédéric Devernay <frederic.devernay@inrialpes.fr>
 *
 * 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 "config.h"

#include <cctype>

#include "debug.h"
#include "exc.h"
#include "msg.h"
#include "str.h"

#include "input.h"


input::input() throw ()
    : _video_width(-1), _video_height(-1), _video_frame_rate_num(-1), _video_frame_rate_den(-1),
    _audio_rate(-1), _audio_channels(-1), _audio_sample_format(decoder::audio_sample_u8), _duration()
{
}

input::~input()
{
}

std::string input::mode_name(enum mode m)
{
    std::string name;
    switch (m)
    {
    case mono:
        name = "mono";
        break;
    case separate:
        name = "separate";
        break;
    case top_bottom:
        name = "top-bottom";
        break;
    case top_bottom_half:
        name = "top-bottom-half";
        break;
    case left_right:
        name = "left-right";
        break;
    case left_right_half:
        name = "left-right-half";
        break;
    case even_odd_rows:
        name = "even-odd-rows";
        break;
    case automatic:
        name = "automatic";
        break;
    }
    return name;
}

enum input::mode input::mode_from_name(const std::string &name, bool *ok)
{
    enum mode m = mono;
    if (ok)
    {
        *ok = true;
    }
    if (name == "mono")
    {
        m = mono;
    }
    else if (name == "separate")
    {
        m = separate;
    }
    else if (name == "top-bottom")
    {
        m = top_bottom;
    }
    else if (name == "top-bottom-half")
    {
        m = top_bottom_half;
    }
    else if (name == "left-right")
    {
        m = left_right;
    }
    else if (name == "left-right-half")
    {
        m = left_right_half;
    }
    else if (name == "even-odd-rows")
    {
        m = even_odd_rows;
    }
    else if (name == "automatic")
    {
        m = automatic;
    }
    else
    {
        if (ok)
        {
            *ok = false;
        }
    }
    return m;
}

bool input::mode_is_2d(enum input::mode m)
{
    return (m == mono);
}

void input::open(std::vector<decoder *> decoders,
        int video0_decoder, int video0_stream,
        int video1_decoder, int video1_stream,
        int audio_decoder, int audio_stream,
        enum mode mode)
{
    const char *tag_value;

    if (video0_decoder == -1)
    {
        throw exc("no video stream");
    }
    if (mode == separate && video1_decoder == -1)
    {
        throw exc("need two video streams");
    }
    if ((mode == left_right || mode == left_right_half)
            && decoders.at(video0_decoder)->video_width(video0_stream) % 2 != 0)
    {
        throw exc("invalid width of left-right video stream");
    }
    if ((mode == top_bottom || mode == top_bottom_half)
            && decoders.at(video0_decoder)->video_height(video0_stream) % 2 != 0)
    {
        throw exc("invalid height of top-bottom video stream");
    }
    if (mode == even_odd_rows && decoders.at(video0_decoder)->video_height(video0_stream) % 2 != 0)
    {
        throw exc("invalid height of even-odd-rows video stream");
    }
    if (video1_decoder != -1)
    {
        if (decoders.at(video1_decoder)->video_width(video1_stream)
                != decoders.at(video0_decoder)->video_width(video0_stream)
            || decoders.at(video1_decoder)->video_height(video1_stream)
                != decoders.at(video0_decoder)->video_height(video0_stream))
        {
            throw exc("video streams have different dimensions");
        }
        if (decoders.at(video1_decoder)->video_aspect_ratio_numerator(video1_stream)
                != decoders.at(video0_decoder)->video_aspect_ratio_numerator(video0_stream)
                || decoders.at(video1_decoder)->video_aspect_ratio_denominator(video1_stream)
                != decoders.at(video0_decoder)->video_aspect_ratio_denominator(video0_stream))
        {
            throw exc("video streams have different aspect ratios");
        }
        if (decoders.at(video1_decoder)->video_frame_rate_numerator(video1_stream)
                != decoders.at(video0_decoder)->video_frame_rate_numerator(video0_stream)
            || decoders.at(video1_decoder)->video_frame_rate_denominator(video1_stream)
                > decoders.at(video0_decoder)->video_frame_rate_denominator(video0_stream))
        {
            throw exc("video streams have different frame rates");
        }
        if (decoders.at(video1_decoder)->video_preferred_frame_format(video1_stream)
                != decoders.at(video0_decoder)->video_preferred_frame_format(video0_stream))
        {
            throw exc("video streams have different preferred frame formats");
        }
    }
    _mode = mode;
    _swap_eyes = false;
    _initial_skip = 0;
    _decoders = decoders;
    _video_decoders[0] = video0_decoder;
    _video_streams[0] = video0_stream;
    _video_decoders[1] = video1_decoder;
    _video_streams[1] = video1_stream;
    _audio_decoder = audio_decoder;
    _audio_stream = audio_stream;
    _video_width = _decoders.at(_video_decoders[0])->video_width(_video_streams[0]);
    _video_height = _decoders.at(_video_decoders[0])->video_height(_video_streams[0]);
    _video_aspect_ratio = static_cast<float>(_decoders.at(_video_decoders[0])->video_aspect_ratio_numerator(_video_streams[0]))
        / static_cast<float>(_decoders.at(_video_decoders[0])->video_aspect_ratio_denominator(_video_streams[0]));
    /* Check some tags defined at this link: http://www.3dtv.at/Knowhow/StereoWmvSpec_en.aspx
     * This is necessary to make the example movies provided by 3dtv.at work out of the box. */
    if ((tag_value = _decoders.at(_video_decoders[0])->tag_value("StereoscopicSkip")))
    {
        try
        {
            _initial_skip = str::to<int64_t>(tag_value);
        }
        catch (...)
        {
        }
    }
    if ((tag_value = _decoders.at(_video_decoders[0])->tag_value("StereoscopicLayout")))
    {
        if (std::string(tag_value) == "SideBySideRF" || std::string(tag_value) == "OverUnderRT")
        {
            _swap_eyes = true;
        }
    }
    /* If we have more than one video stream, the input mode must be "separate". */
    if (_mode == automatic)
    {
        if (video1_decoder != -1)
        {
            _mode = separate;
        }
    }
    /* First, try to determine the input mode from meta data if none is given. */
    if (_mode == automatic)
    {
        if ((tag_value = _decoders.at(_video_decoders[0])->tag_value("StereoscopicLayout")))
        {
            if (std::string(tag_value) == "SideBySideRF" || std::string(tag_value) == "SideBySideLF")
            {
                _mode = left_right;
                if ((tag_value = _decoders.at(_video_decoders[0])->tag_value("StereoscopicHalfWidth"))
                        && std::string(tag_value) == "1")
                {
                    _mode = left_right_half;
                }
            }
            else if (std::string(tag_value) == "OverUnderRT" || std::string(tag_value) == "OverUnderLT")
            {
                _mode = top_bottom;
                if ((tag_value = _decoders.at(_video_decoders[0])->tag_value("StereoscopicHalfHeight"))
                        && std::string(tag_value) == "1")
                {
                    _mode = top_bottom_half;
                }
            }
        }
    }
    /* If that fails, try to determine the input mode by looking at the file name.
     * These are the file name conventions described here:
     * http://www.tru3d.com/technology/3D_Media_Formats_Software.php?file=TriDef%20Supported%203D%20Formats */
    if (_mode == automatic)
    {
        std::string name = _decoders.at(video0_decoder)->file_name();
        size_t last_dot = name.find_last_of('.');
        name = name.substr(0, last_dot);
        for (size_t i = 0; i < name.length(); i++)
        {
            name[i] = std::tolower(name[i]);
        }
        if (name.length() >= 3 && name.substr(name.length() - 3, 3) == "-lr")
        {
            _mode = left_right;
        }
        else if (name.length() >= 3 && name.substr(name.length() - 3, 3) == "-rl")
        {
            _mode = left_right;
            _swap_eyes = true;
        }
        else if (name.length() >= 4
                && (name.substr(name.length() - 4, 4) == "-lrh"
                    || name.substr(name.length() - 4, 4) == "-lrq"))
        {
            _mode = left_right_half;
        }
        else if (name.length() >= 4
                && (name.substr(name.length() - 4, 4) == "-rlh"
                    || name.substr(name.length() - 4, 4) == "-rlq"))
        {
            _mode = left_right_half;
            _swap_eyes = true;
        }
        else if (name.length() >= 3
                && (name.substr(name.length() - 3, 3) == "-tb"
                    || name.substr(name.length() - 3, 3) == "-ab"))
        {
            _mode = top_bottom;
        }
        else if (name.length() >= 3
                && (name.substr(name.length() - 3, 3) == "-bt"
                    || name.substr(name.length() - 3, 3) == "-ba"))
        {
            _mode = top_bottom;
            _swap_eyes = true;
        }
        else if (name.length() >= 4
                && (name.substr(name.length() - 4, 4) == "-tbh"
                    || name.substr(name.length() - 4, 4) == "-abq"))
        {
            _mode = top_bottom_half;
        }
        else if (name.length() >= 4
                && (name.substr(name.length() - 4, 4) == "-bth"
                    || name.substr(name.length() - 4, 4) == "-baq"))
        {
            _mode = top_bottom_half;
            _swap_eyes = true;
        }
        else if (name.length() >= 3 && name.substr(name.length() - 3, 3) == "-eo")
        {
            _mode = even_odd_rows;
            // all image lines are given in this case, and there should be no interpolation [TODO]
        }
        else if (name.length() >= 3 && name.substr(name.length() - 3, 3) == "-oe")
        {
            _mode = even_odd_rows;
            _swap_eyes = true;
            // all image lines are given in this case, and there should be no interpolation [TODO]
        }
        else if ((name.length() >= 4 && name.substr(name.length() - 4, 4) == "-eoq")
            || (name.length() >= 5 && name.substr(name.length() - 5, 5) == "-3dir"))
        {
            _mode = even_odd_rows;
        }
        else if ((name.length() >= 4 && name.substr(name.length() - 4, 4) == "-oeq")
            || (name.length() >= 4 && name.substr(name.length() - 4, 4) == "-3di"))
        {
            _mode = even_odd_rows;
            _swap_eyes = true;
        }
        else if (name.length() >= 3 && name.substr(name.length() - 3, 3) == "-2d")
        {
            _mode = mono;
        }
    }
    /* If that fails, too, try to determine the input mode from the resolution.*/
    if (_mode == automatic)
    {
        if (_video_width > _video_height)
        {
            if (_video_width / 2 > _video_height)
            {
                _mode = left_right;
            }
            else
            {
                _mode = mono;
            }
        }
        else
        {
            _mode = top_bottom;
        }
    }
    /* At this point, _mode != automatic */

    if (_mode == left_right)
    {
        _video_width /= 2;
        _video_aspect_ratio /= 2.0f;
    }
    else if (_mode == left_right_half)
    {
        _video_width /= 2;
    }
    else if (_mode == top_bottom)
    {
        _video_height /= 2;
        _video_aspect_ratio *= 2.0f;
    }
    else if (_mode == top_bottom_half)
    {
        _video_height /= 2;
    }
    else if (_mode == even_odd_rows)
    {
        _video_height /= 2;
        //_video_aspect_ratio *= 2.0f;
        // The only video files I know of which use row-alternating format (those from stereopia.com)
        // do not want this adjustment of aspect ratio.
    }
    _video_frame_rate_num = _decoders.at(_video_decoders[0])->video_frame_rate_numerator(_video_streams[0]);
    _video_frame_rate_den = _decoders.at(_video_decoders[0])->video_frame_rate_denominator(_video_streams[0]);
    _video_preferred_frame_format = _decoders.at(_video_decoders[0])->video_preferred_frame_format(_video_streams[0]);

    if (audio_stream != -1)
    {
        _audio_rate = _decoders.at(_audio_decoder)->audio_rate(_audio_stream);
        _audio_channels = _decoders.at(_audio_decoder)->audio_channels(_audio_stream);
        _audio_sample_format = _decoders.at(_audio_decoder)->audio_sample_format(_audio_stream);
    }

    _decoders.at(_video_decoders[0])->activate_video_stream(_video_streams[0]);
    _duration = _decoders.at(_video_decoders[0])->video_duration(_video_streams[0]);
    if (_video_streams[1] != -1)
    {
        _decoders.at(_video_decoders[1])->activate_video_stream(_video_streams[1]);
        _duration = std::min(_duration, _decoders.at(_video_decoders[1])->video_duration(_video_streams[1]));
    }
    if (_audio_stream != -1)
    {
        _decoders.at(_audio_decoder)->activate_audio_stream(_audio_stream);
        _duration = std::min(_duration, _decoders.at(_audio_decoder)->audio_duration(_audio_stream));
    }

    // Skip the initial advertisement in 3dtv.at examples.
    if (_initial_skip > 0)
    {
        _duration -= _initial_skip;
        try
        {
            seek(0);
        }
        catch (...)
        {
            _duration += _initial_skip;
        }
    }

    msg::dbg("video0=%d,%d video1=%d,%d, audio=%d,%d",
            _video_decoders[0], _video_streams[0],
            _video_decoders[1], _video_streams[1],
            _audio_decoder, _audio_stream);
    msg::inf("input:");
    msg::inf("    video: %dx%d, format %s,",
            video_width(), video_height(),
            decoder::video_frame_format_name(video_preferred_frame_format()).c_str());
    msg::inf("        aspect ratio %g:1, %g fps, %g seconds,",
            video_aspect_ratio(),
            static_cast<float>(video_frame_rate_numerator()) / static_cast<float>(video_frame_rate_denominator()),
            duration() / 1e6f);
    msg::inf("        stereo mode %s, input eye swap %s",
            (_mode == separate ? "separate-streams"
             : _mode == top_bottom ? "top-bottom" : _mode == top_bottom_half ?  "top-bottom-half"
             : _mode == left_right ? "left-right" : _mode == left_right_half ?  "left-right-half"
             : _mode == even_odd_rows ? "even-odd-rows" : "off"),
            (_swap_eyes ? "on" : "off"));

    if (audio_stream != -1)
    {
        msg::inf("    audio: %d channels, %d Hz, sample format %s",
                audio_channels(), audio_rate(), decoder::audio_sample_format_name(audio_sample_format()).c_str());
    }
    else
    {
        msg::inf("    audio: none");
    }
}

int64_t input::read_video_frame()
{
    int64_t t = _decoders.at(_video_decoders[0])->read_video_frame(_video_streams[0]);
    if (t < 0)
    {
        return t;
    }
    if (_video_decoders[1] != -1)
    {
        int64_t t2 = _decoders.at(_video_decoders[1])->read_video_frame(_video_streams[1]);
        if (t2 < 0)
        {
            return t2;
        }
    }
    return t;
}

void input::get_video_frame(enum decoder::video_frame_format fmt,
        uint8_t *l_data[3], size_t l_line_size[3],
        uint8_t *r_data[3], size_t r_line_size[3])
{
    uint8_t *data[3], *data1[3];
    size_t line_size[3], line_size1[3];

    _decoders.at(_video_decoders[0])->get_video_frame(_video_streams[0], fmt, data, line_size);
    switch (_mode)
    {
    case separate:
        _decoders.at(_video_decoders[1])->get_video_frame(_video_streams[1], fmt, data1, line_size1);
        l_data[0] = data[0];
        l_data[1] = data[1];
        l_data[2] = data[2];
        l_line_size[0] = line_size[0];
        l_line_size[1] = line_size[1];
        l_line_size[2] = line_size[2];
        r_data[0] = data1[0];
        r_data[1] = data1[1];
        r_data[2] = data1[2];
        r_line_size[0] = line_size1[0];
        r_line_size[1] = line_size1[1];
        r_line_size[2] = line_size1[2];
        break;
    case top_bottom:
    case top_bottom_half:
        l_data[0] = data[0];
        l_data[1] = data[1];
        l_data[2] = data[2];
        l_line_size[0] = line_size[0];
        l_line_size[1] = line_size[1];
        l_line_size[2] = line_size[2];
        r_data[0] = l_data[0] + video_height() * line_size[0];
        r_data[1] = l_data[1] + video_height() / 2 * line_size[1];
        r_data[2] = l_data[2] + video_height() / 2 * line_size[2];
        r_line_size[0] = l_line_size[0];
        r_line_size[1] = l_line_size[1];
        r_line_size[2] = l_line_size[2];
        break;
    case left_right:
    case left_right_half:
        l_data[0] = data[0];
        l_data[1] = data[1];
        l_data[2] = data[2];
        l_line_size[0] = line_size[0];
        l_line_size[1] = line_size[1];
        l_line_size[2] = line_size[2];
        r_data[0] = data[0] + video_width() * (fmt == decoder::frame_format_yuv420p ? 1 : 4);
        r_data[1] = data[1] + video_width() / 2;        // irrelevant for bgra32
        r_data[2] = data[2] + video_width() / 2;        // irrelevant for bgra32
        r_line_size[0] = line_size[0];
        r_line_size[1] = line_size[1];
        r_line_size[2] = line_size[2];
        break;
    case even_odd_rows:
        l_data[0] = data[0];
        l_data[1] = data[1];
        l_data[2] = data[2];
        l_line_size[0] = 2 * line_size[0];
        l_line_size[1] = 2 * line_size[1];
        l_line_size[2] = 2 * line_size[2];
        r_data[0] = data[0] + line_size[0];
        r_data[1] = data[1] + line_size[1];
        r_data[2] = data[2] + line_size[2];
        r_line_size[0] = 2 * line_size[0];
        r_line_size[1] = 2 * line_size[1];
        r_line_size[2] = 2 * line_size[2];
        break;
    case mono:
        l_data[0] = data[0];
        l_data[1] = data[1];
        l_data[2] = data[2];
        l_line_size[0] = line_size[0];
        l_line_size[1] = line_size[1];
        l_line_size[2] = line_size[2];
        r_data[0] = data[0];
        r_data[1] = data[1];
        r_data[2] = data[2];
        r_line_size[0] = line_size[0];
        r_line_size[1] = line_size[1];
        r_line_size[2] = line_size[2];
        break;
    case automatic:
        /* cannot happen */
        break;
    }
    if (_swap_eyes)
    {
        std::swap(l_data[0], r_data[0]);
        std::swap(l_data[1], r_data[1]);
        std::swap(l_data[2], r_data[2]);
        std::swap(l_line_size[0], r_line_size[0]);
        std::swap(l_line_size[1], r_line_size[1]);
        std::swap(l_line_size[2], r_line_size[2]);
    }
}

void input::release_video_frame()
{
    _decoders.at(_video_decoders[0])->release_video_frame(_video_streams[0]);
    if (_video_decoders[1] != -1)
    {
        _decoders.at(_video_decoders[1])->release_video_frame(_video_streams[1]);
    }
}

int64_t input::read_audio_data(void **data, size_t size)
{
    if (_audio_buffer.size() < size)
    {
        _audio_buffer.resize(size);
    }
    *data = _audio_buffer.ptr();
    return _decoders.at(_audio_decoder)->read_audio_data(_audio_stream, _audio_buffer.ptr(), size);
}

void input::seek(int64_t dest_pos)
{
    dest_pos += _initial_skip;
    _decoders.at(_video_decoders[0])->seek(dest_pos);
    if (_video_decoders[1] != -1 && _video_decoders[1] != _video_decoders[0])
    {
        _decoders.at(_video_decoders[1])->seek(dest_pos);
    }
    if (_audio_decoder != -1 && _audio_decoder != _video_decoders[0] && _audio_decoder != _video_decoders[1])
    {
        _decoders.at(_audio_decoder)->seek(dest_pos);
    }
}

void input::close()
{
}
