/*****************************************************************************
 * matroska_segment.hpp : matroska demuxer
 *****************************************************************************
 * Copyright (C) 2016 VLC authors and VideoLAN
 *
 * Authors: Filip Roséen <filip@videolabs.io>
 *
 * 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 2.1 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.
 *****************************************************************************/

#include "matroska_segment_seeker.hpp"
#include "matroska_segment.hpp"
#include "demux.hpp"
#include "Ebml_parser.hpp"
#include "Ebml_dispatcher.hpp"
#include "util.hpp"
#include "stream_io_callback.hpp"

#include <sstream>
#include <limits>

namespace {
    template<class It, class T>
    It greatest_lower_bound( It beg, It end, T const& value )
    {
        It it = std::upper_bound( beg, end, value );
        if( it != beg ) --it;
        return it;
    }

    // std::prev and std::next exists in C++11, in order to avoid ambiguity due
    // to ADL and iterators being defined within namespace std, these two
    // function-names have been postfixed with an underscore.

    template<class It> It prev_( It it ) { return --it; }
    template<class It> It next_( It it ) { return ++it; }
}

namespace mkv {

SegmentSeeker::cluster_positions_t::iterator
SegmentSeeker::add_cluster_position( fptr_t fpos )
{
    cluster_positions_t::iterator insertion_point = std::upper_bound(
      _cluster_positions.begin(),
      _cluster_positions.end(),
      fpos
    );

    return _cluster_positions.insert( insertion_point, fpos );
}

SegmentSeeker::cluster_map_t::iterator
SegmentSeeker::add_cluster( KaxCluster * const p_cluster )
{
    Cluster cinfo = {
        /* fpos     */ p_cluster->GetElementPosition(),
        /* pts      */ vlc_tick_t( VLC_TICK_FROM_NS( p_cluster->GlobalTimestamp() ) ),
        /* duration */ vlc_tick_t( -1 ),
        /* size     */ p_cluster->IsFiniteSize()
            ? p_cluster->GetEndPosition() - p_cluster->GetElementPosition()
            : UINT64_MAX
    };

    add_cluster_position( cinfo.fpos );

    cluster_map_t::iterator it = _clusters.lower_bound( cinfo.pts );

    if( it != _clusters.end() && it->second.pts == cinfo.pts )
    {
        // cluster already known
    }
    else
    {
        it = _clusters.insert( cluster_map_t::value_type( cinfo.pts, cinfo ) ).first;
    }

    // ------------------------------------------------------------------
    // IF we have two adjecent clusters, update duration where applicable
    // ------------------------------------------------------------------

    struct Duration {
        static void fix( Cluster& prev, Cluster& next )
        {
            if( ( prev.fpos + prev.size) == next.fpos )
                prev.duration = next.pts - prev.pts;
        }
    };

    if( it != _clusters.begin() )
    {
        Duration::fix( prev_( it )->second, it->second );
    }

    if( it != _clusters.end() && next_( it ) != _clusters.end() )
    {
        Duration::fix( it->second, next_( it )->second );
    }

    return it;
}

void
SegmentSeeker::add_seekpoint( track_id_t track_id, Seekpoint sp )
{
    seekpoints_t&  seekpoints = _tracks_seekpoints[ track_id ];
    seekpoints_t::iterator it = std::lower_bound( seekpoints.begin(), seekpoints.end(), sp );

    if( it != seekpoints.end() && it->fpos == sp.fpos )
    {
        if (sp.trust_level <= it->trust_level)
            return;

        *it = sp;
    }
    else if( it != seekpoints.end() && it->pts == sp.pts )
    {
        if (sp.trust_level <= it->trust_level)
            return;

        *it = sp;
    }
    else
    {
        seekpoints.insert( it, sp );
    }
}

SegmentSeeker::tracks_seekpoint_t
SegmentSeeker::find_greatest_seekpoints_in_range( fptr_t start_fpos, vlc_tick_t end_pts, track_ids_t const& filter_tracks )
{
    tracks_seekpoint_t tpoints;

    for( tracks_seekpoints_t::const_iterator it = _tracks_seekpoints.begin(); it != _tracks_seekpoints.end(); ++it )
    {
        if ( std::find( filter_tracks.begin(), filter_tracks.end(), it->first ) == filter_tracks.end() )
            continue;

        Seekpoint sp = get_first_seekpoint_around( end_pts, it->second );

        if( sp.fpos < start_fpos )
            continue;

        if( sp.pts > end_pts )
            continue;

        tpoints.insert( tracks_seekpoint_t::value_type( it->first, sp ) );
    }

    if (tpoints.empty())
    {
        // try a further pts
        for( tracks_seekpoints_t::const_iterator it = _tracks_seekpoints.begin(); it != _tracks_seekpoints.end(); ++it )
        {
            if ( std::find( filter_tracks.begin(), filter_tracks.end(), it->first ) == filter_tracks.end() )
                continue;

            Seekpoint sp = get_first_seekpoint_around( end_pts, it->second );

            if( sp.fpos < start_fpos )
                continue;

            tpoints.insert( tracks_seekpoint_t::value_type( it->first, sp ) );
        }
    }

    return tpoints;
}

SegmentSeeker::Seekpoint
SegmentSeeker::get_first_seekpoint_around( vlc_tick_t pts, seekpoints_t const& seekpoints,
                                           Seekpoint::TrustLevel trust_level )
{
    if( seekpoints.empty() )
    {
        return Seekpoint();
    }

    typedef seekpoints_t::const_iterator iterator;

    Seekpoint const needle ( std::numeric_limits<fptr_t>::max(), pts );

    iterator const it_begin  = seekpoints.begin();
    iterator const it_end    = seekpoints.end();
    iterator const it_middle = greatest_lower_bound( it_begin, it_end, needle );

    iterator it_before;

    // rewrind to _previous_ seekpoint with appropriate trust
    for( it_before = it_middle; it_before != it_begin; --it_before )
    {
        if( it_before->trust_level >= trust_level )
            return *it_before;
    }
    return *it_begin;
}

SegmentSeeker::seekpoint_pair_t
SegmentSeeker::get_seekpoints_around( vlc_tick_t pts, seekpoints_t const& seekpoints )
{
    if( seekpoints.empty() )
    {
        return seekpoint_pair_t();
    }

    typedef seekpoints_t::const_iterator iterator;

    Seekpoint const needle ( std::numeric_limits<fptr_t>::max(), pts );

    iterator const it_begin  = seekpoints.begin();
    iterator const it_end    = seekpoints.end();
    iterator const it_middle = greatest_lower_bound( it_begin, it_end, needle );

    if ( it_middle != it_end && (*it_middle).pts > pts)
        // found nothing low enough, use the first one
        return seekpoint_pair_t( *it_begin, Seekpoint() );

    iterator it_before = it_middle;
    iterator it_after = it_middle == it_end ? it_middle : next_( it_middle ) ;

    return seekpoint_pair_t( *it_before,
      it_after == it_end ? Seekpoint() : *it_after
    );
}

SegmentSeeker::seekpoint_pair_t
SegmentSeeker::get_seekpoints_around( vlc_tick_t target_pts, track_ids_t const& priority_tracks )
{
    seekpoint_pair_t points;

    if( _tracks_seekpoints.empty() )
        return points;

    { // locate the max/min seekpoints for priority_tracks //

        typedef track_ids_t::const_iterator track_iterator;

        track_iterator const begin = priority_tracks.begin();
        track_iterator const end   = priority_tracks.end();

        for( track_iterator it = begin; it != end; ++it )
        {
            seekpoint_pair_t track_points = get_seekpoints_around( target_pts, _tracks_seekpoints[ *it ] );

            if( it == begin ) {
                points = track_points;
                continue;
            }

            if( track_points.first.trust_level > Seekpoint::DISABLED &&
                points.first.fpos > track_points.first.fpos )
                points.first = track_points.first;

            if( track_points.second.trust_level > Seekpoint::DISABLED &&
                points.second.fpos < track_points.second.fpos )
                points.second = track_points.second;
        }
    }

    { // check if we got a cluster which is closer to target_pts than the found cues //

        cluster_map_t::iterator it = _clusters.lower_bound( target_pts );

        if( it != _clusters.begin() && --it != _clusters.end() )
        {
            Cluster const& cluster = it->second;

            if( cluster.fpos > points.first.fpos )
            {
                points.first = Seekpoint( cluster.fpos, cluster.pts );

                // do we need to update the max point? //

                if( points.second.fpos < points.first.fpos )
                    points.second = Seekpoint( cluster.fpos + cluster.size, cluster.pts + cluster.duration );
            }
        }
    }

    return points;
}

SegmentSeeker::tracks_seekpoint_t
SegmentSeeker::get_seekpoints( matroska_segment_c& ms, vlc_tick_t target_pts,
                               track_ids_t const& priority_tracks, track_ids_t const& filter_tracks )
{
    struct contains_all_of_t {
        bool operator()( tracks_seekpoint_t const& haystack, track_ids_t const& track_ids )
        {
            for( track_ids_t::const_iterator it = track_ids.begin(); it != track_ids.end(); ++it ) {
                if( haystack.find( *it ) == haystack.end() )
                    return false;
            }

            return true;
        }
    };

    for( vlc_tick_t needle_pts = target_pts; ; )
    {
        seekpoint_pair_t seekpoints = get_seekpoints_around( needle_pts, priority_tracks );

        Seekpoint const& start = seekpoints.first;
        Seekpoint const& end   = seekpoints.second;

        if ( start.fpos == std::numeric_limits<fptr_t>::max() )
            return tracks_seekpoint_t();

        if ( ms.sys.b_fastseekable &&
             (end.fpos != std::numeric_limits<fptr_t>::max() || !ms.b_cues) &&
             (needle_pts != start.pts || start.trust_level < Seekpoint::TRUSTED))
            // do not read the whole (infinite?) file to get seek indexes
            // do not generate an index if we already have the correct seekpoint
            index_range( ms, Range( start.fpos, end.fpos ), needle_pts );

        tracks_seekpoint_t tpoints = find_greatest_seekpoints_in_range( start.fpos, target_pts, filter_tracks );

        if( contains_all_of_t() ( tpoints, priority_tracks ) )
            return tpoints;

        // Avoid busyloop, don't iterate on the same seekpoint
        if( needle_pts == start.pts - 1 )
            // we found the same needle twice, stop looking
            return tpoints;

        needle_pts = start.pts - 1;
    }

    vlc_assert_unreachable();
}

void
SegmentSeeker::index_range( matroska_segment_c& ms, Range search_area, vlc_tick_t max_pts )
{
    ranges_t areas_to_search = get_search_areas( search_area.start, search_area.end );

    for( ranges_t::const_iterator range_it = areas_to_search.begin(); range_it != areas_to_search.end(); ++range_it )
        index_unsearched_range( ms, *range_it, max_pts );
}

void
SegmentSeeker::index_unsearched_range( matroska_segment_c& ms, Range search_area, vlc_tick_t max_pts )
{
    mkv_jump_to( ms, search_area.start );

    search_area.start = ms.es.I_O().getFilePointer();

    fptr_t  block_pos = search_area.start;
    vlc_tick_t block_pts;

    while( block_pos < search_area.end )
    {
        KaxBlock * block;
        KaxSimpleBlock * simpleblock;
        KaxBlockAdditions *additions;

        bool     b_key_picture;
        bool     b_discardable_picture;
        int64_t  i_block_duration;
        track_id_t track_id;

        if( ms.BlockGet( block, simpleblock, additions,
                         &b_key_picture, &b_discardable_picture, &i_block_duration ) )
        {
            delete additions;
            break;
        }
        delete additions;

        KaxInternalBlock& internal_block = simpleblock
            ? static_cast<KaxInternalBlock&>( *simpleblock )
            : static_cast<KaxInternalBlock&>( *block );

        block_pos = internal_block.GetElementPosition();
        block_pts = VLC_TICK_FROM_NS(internal_block.GlobalTimestamp());
        track_id  = internal_block.TrackNum();

        bool const b_valid_track = ms.FindTrackByBlock( block, simpleblock ) != NULL;

        delete block;

        if( b_valid_track )
        {
            if( b_key_picture )
                add_seekpoint( track_id, Seekpoint( block_pos, block_pts ) );

            if( max_pts < block_pts )
                break;
        }
    }

    search_area.end = ms.es.I_O().getFilePointer();

    mark_range_as_searched( search_area );
}

void
SegmentSeeker::mark_range_as_searched( Range data )
{
    /* TODO: this is utterly ugly, we should do the insertion in-place */

    _ranges_searched.insert( std::upper_bound( _ranges_searched.begin(), _ranges_searched.end(), data ), data );

    {
        ranges_t merged;

        for( ranges_t::iterator it = _ranges_searched.begin(); it != _ranges_searched.end(); ++it )
        {
            if( merged.size() )
            {
                Range& last_entry = *merged.rbegin();

                if( last_entry.end+1 >= it->start && last_entry.end < it->end )
                {
                    last_entry.end = it->end;
                    continue;
                }

                if( it->start >= last_entry.start && it->end <= last_entry.end )
                {
                    last_entry.end = std::max( last_entry.end, it->end );
                    continue;
                }
            }

            merged.push_back( *it );
        }

        _ranges_searched = std::move(merged);
    }
}


SegmentSeeker::ranges_t
SegmentSeeker::get_search_areas( fptr_t start, fptr_t end ) const
{
    ranges_t areas_to_search;
    Range needle ( start, end );

    ranges_t::const_iterator it = greatest_lower_bound( _ranges_searched.begin(), _ranges_searched.end(), needle );

    for( ; it != _ranges_searched.end() && needle.start < needle.end; ++it )
    {
        if( needle.start < it->start )
        {
            areas_to_search.push_back( Range( needle.start, it->start ) );
        }

        if( needle.start <= it->end )
            needle.start = it->end + 1;
    }

    needle.start = std::max( needle.start, start );
    if( it == _ranges_searched.end() && needle.start < needle.end )
    {
        areas_to_search.push_back( needle );
    }

    return areas_to_search;
}

void
SegmentSeeker::mkv_jump_to( matroska_segment_c& ms, fptr_t fpos )
{
    fptr_t i_cluster_pos = -1;

    if ( fpos != std::numeric_limits<SegmentSeeker::fptr_t>::max() )
    {
        if ( !_cluster_positions.empty() )
        {
            cluster_positions_t::iterator cluster_it = greatest_lower_bound(
              _cluster_positions.begin(), _cluster_positions.end(), fpos
            );

            ms.es.I_O().setFilePointer( *cluster_it );
            ms.ep.reconstruct( &ms.es, ms.segment, &ms.sys.demuxer );

            for(;;)
            {
                ms.cluster = NULL;
                EbmlElement *el = ms.ep.Get();
                if( el == nullptr )
                {
                    msg_Err( &ms.sys.demuxer, "unable to read KaxCluster during seek, giving up" );
                    return;
                }
                if (!MKV_IS_ID( el, KaxCluster ))
                    continue; // look for the next element

                ms.cluster = static_cast<KaxCluster*>( el );

                i_cluster_pos = ms.cluster->GetElementPosition();

                add_cluster_position( i_cluster_pos );

                mark_range_as_searched( Range( i_cluster_pos, ms.es.I_O().getFilePointer() ) );

                if ( !ms.cluster->IsFiniteSize() || ms.cluster->GetEndPosition() >= fpos)
                    break;
            }
        }
    }
    else if (ms.cluster != NULL)
    {
        // make sure we start reading after the Cluster start
        ms.es.I_O().setFilePointer(ms.cluster->GetDataStart());
    }

    assert(ms.ep.GetLevel() == 1);
    ms.ep.Down();

    /* read until cluster/timecode to initialize cluster */

    while( EbmlElement * el = ms.ep.Get() )
    {
        try {
            if( MKV_CHECKED_PTR_DECL( p_tc, KaxClusterTimestamp, el ) )
            {
                p_tc->ReadData( ms.es.I_O(), SCOPE_ALL_DATA );
                ms.cluster->InitTimestamp( static_cast<uint64_t>( *p_tc ), ms.i_timescale );
                add_cluster(ms.cluster);
                break;
            }
            else if( MKV_CHECKED_PTR_DECL( crc, EbmlCrc32, el ) )
            {
                crc->ReadData( ms.es.I_O(), SCOPE_ALL_DATA ); /* avoid a skip that may fail */
            }
        }
        catch(...)
        {
            msg_Err( &ms.sys.demuxer,"Error while reading %s",  EBML_NAME(el) );
        }
    }

    /* TODO: add error handling; what if we never get a KaxCluster and/or KaxClusterTimestamp? */

    mark_range_as_searched( Range( i_cluster_pos, ms.es.I_O().getFilePointer() ) );

    /* jump to desired position */

    if ( fpos != std::numeric_limits<SegmentSeeker::fptr_t>::max() )
        ms.es.I_O().setFilePointer( fpos );
}

} // namespace
