/*
 * filter_gpsgraphic.cpp -- draws gps related graphics
 * Copyright (c) 2015-2025 Meltytech, LLC
 * Original author: Daniel F
 *
 * This library 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 library 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 library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
#include "filter_gpsgraphic.h"
#include "common.h"

#include <QMutex>
static QMutex f_mutex;

// Sets the private data to default values and frees gps points array
static void default_priv_data(private_data *pdata)
{
    if (pdata) {
        if (pdata->gps_points_r) {
            free(pdata->gps_points_r);
            pdata->gps_points_r = NULL;
        }
        if (pdata->gps_points_p) {
            free(pdata->gps_points_p);
            pdata->gps_points_p = NULL;
        }
        pdata->gps_points_size = 0;
        pdata->last_smooth_lvl = 0;
        pdata->last_searched_index = 0;
        pdata->first_gps_time = 0;
        pdata->last_gps_time = 0;
        pdata->gps_offset = 0;
        pdata->speed_multiplier = 1.0;
        pdata->last_filename[0] = '\0';
        pdata->interpolated = 0;
        pdata->minmax.set_defaults();
        memset(&pdata->ui_crops, 0, sizeof(pdata->ui_crops));
        pdata->graph_data_source = 0;
        pdata->graph_type = 0;
        memset(&pdata->img_rect, 0, sizeof(pdata->img_rect));
        pdata->last_bg_img_path[0] = '\0';
        pdata->map_aspect_ratio_from_distance = 0.0;
        pdata->bg_img = QImage();
        pdata->bg_img_scaled = QImage();
        pdata->bg_img_scaled_width = 0.0;
        pdata->bg_img_scaled_height = 0.0;
        pdata->bg_img_matched_rect = QRectF();
        pdata->swap_180 = 0;
    }
}

//like decimals_needed() but graph data source aware
int decimals_needed_bysrc(mlt_filter filter, double v)
{
    private_data *pdata = (private_data *) filter->child;

    if (pdata->graph_data_source == gspg_location_src)
        return 6;
    if (pdata->graph_data_source == gpsg_altitude_src || pdata->graph_data_source == gpsg_speed_src)
        return (decimals_needed(v));
    if (pdata->graph_data_source == gpsg_hr_src)
        return 0;
    return 0;
}

//convert_[distance|speed]_to_format but graph data source aware
double convert_bysrc_to_format(mlt_filter filter, double val)
{
    private_data *pdata = (private_data *) filter->child;
    char *legend_unit = mlt_properties_get(MLT_FILTER_PROPERTIES(filter), "legend_unit");

    if (pdata->graph_data_source == gpsg_altitude_src)
        return convert_distance_to_format(val, legend_unit);
    if (pdata->graph_data_source == gpsg_speed_src)
        return convert_speed_to_format(val, legend_unit);
    return val;
}

//helps pass filter private data for gps_parser.cpp calls
gps_private_data filter_to_gps_data(mlt_filter filter)
{
    private_data *pdata = (private_data *) filter->child;
    gps_private_data ret;

    ret.gps_points_r = pdata->gps_points_r;
    ret.gps_points_p = pdata->gps_points_p;
    ret.ptr_to_gps_points_r = &pdata->gps_points_r;
    ret.ptr_to_gps_points_p = &pdata->gps_points_p;
    ret.gps_points_size = &pdata->gps_points_size;

    ret.last_searched_index = &pdata->last_searched_index;
    ret.first_gps_time = &pdata->first_gps_time;
    ret.last_gps_time = &pdata->last_gps_time;
    ret.interpolated = &pdata->interpolated;
    ret.swap180 = &pdata->swap_180;

    ret.gps_proc_start_t = 0;
    ret.last_smooth_lvl = pdata->last_smooth_lvl;
    ret.last_filename = pdata->last_filename;
    ret.filter = filter;
    return ret;
}

/* Gets the value from pdata->minmax (or custom|existing gps_point) depending on data_src value
   get_type represents: 0=current_value, 1=max_value, -1=min_value
   subtype is used for longitude (planned more but didn't make sense so far)
   gps_p is used to get (only for get_type=0) the proper value by source from points not in pdata->gps_points_p[] (mostly weighted point)
*/
double get_by_src(mlt_filter filter,
                  int get_type,
                  int i_gps /* = 0 */,
                  int subtype /* = 0 */,
                  gps_point_proc *gps_p /* = NULL */)
{
    private_data *pdata = (private_data *) filter->child;
    if (i_gps < 0 || i_gps >= pdata->gps_points_size)
        return 0;

    if (pdata->graph_data_source == gspg_location_src) {
        if (get_type == -1) {
            if (subtype == gpsg_latitude_id)
                return pdata->minmax.min_lat_projected;
            else if (subtype == gpsg_longitude_id)
                return pdata->minmax.min_lon;
        } else if (get_type == 1) {
            if (subtype == gpsg_latitude_id)
                return pdata->minmax.max_lat_projected;
            else if (subtype == gpsg_longitude_id)
                return pdata->minmax.max_lon;
        } else if (get_type == 0) {
            if (subtype == gpsg_latitude_id) {
                if (gps_p == NULL)
                    return pdata->gps_points_p[i_gps].lat_projected;
                else if (gps_p != NULL)
                    return gps_p->lat_projected;
            } else if (subtype == gpsg_longitude_id) {
                if (gps_p == NULL)
                    return pdata->gps_points_p[i_gps].lon;
                else if (gps_p != NULL)
                    return gps_p->lon;
            }
        }
    } else if (pdata->graph_data_source == gpsg_altitude_src) {
        if (get_type == -1)
            return pdata->minmax.min_ele;
        else if (get_type == 1)
            return pdata->minmax.max_ele;
        else if (get_type == 0) {
            if (gps_p == NULL)
                return pdata->gps_points_p[i_gps].ele;
            else if (gps_p != NULL)
                return gps_p->ele;
        }
    } else if (pdata->graph_data_source == gpsg_hr_src) {
        if (get_type == -1)
            return pdata->minmax.min_hr;
        else if (get_type == 1)
            return pdata->minmax.max_hr;
        else if (get_type == 0) {
            if (gps_p == NULL)
                return pdata->gps_points_p[i_gps].hr;
            else if (gps_p != NULL)
                return gps_p->hr;
        }
    } else if (pdata->graph_data_source == gpsg_speed_src) {
        if (get_type == -1)
            return pdata->minmax.min_speed;
        else if (get_type == 1)
            return pdata->minmax.max_speed;
        else if (get_type == 0) {
            if (gps_p == NULL)
                return pdata->gps_points_p[i_gps].speed;
            else if (gps_p != NULL)
                return gps_p->speed;
        }
    }
    mlt_log_warning(filter,
                    "Invalid combination of arguments to get_by_src: (get_type=%d, i_gps=%d, "
                    "subtype=%d, gps_p=%p)\n",
                    get_type,
                    i_gps,
                    subtype,
                    gps_p);
    return 0;
}
//get_by_src helper shortcuts
double get_min_bysrc(mlt_filter filter, int subtype /* = 0 */)
{
    return get_by_src(filter, -1, 0, subtype, NULL);
}
double get_max_bysrc(mlt_filter filter, int subtype /* = 0 */)
{
    return get_by_src(filter, 1, 0, subtype, NULL);
}
double get_crtval_bysrc(mlt_filter filter,
                        int i_gps,
                        int subtype /* = 0 */,
                        gps_point_proc *gps_p /* = NULL */)
{
    return get_by_src(filter, 0, i_gps, subtype, gps_p);
}

// Returns the unix time (miliseconds) of "Media Created" metadata, or fallbacks to "Modified Time" from OS
static int64_t get_original_video_file_time_mseconds(mlt_frame frame)
{
    mlt_producer producer = mlt_producer_cut_parent(mlt_frame_get_original_producer(frame));
    return mlt_producer_get_creation_time(producer);
}

/** Returns absolute* current frame time in miliseconds (original file creation + current timecode)
 *  *also applies speed_multiplier
 */
static int64_t get_current_frame_time_ms(mlt_filter filter, mlt_frame frame)
{
    mlt_properties properties = MLT_FILTER_PROPERTIES(filter);
    private_data *pdata = (private_data *) filter->child;
    int64_t file_time = 0, fr_time = 0;

    file_time = get_original_video_file_time_mseconds(frame);
    mlt_position frame_position = mlt_frame_original_position(frame);
    // mlt_log_info(filter, "frame_pos=%d, frame_orig_pos=%d; file_time=%d\n", mlt_frame_get_position(frame), mlt_frame_original_position(frame), file_time/1000);
    f_mutex.lock();
    char *s = mlt_properties_frames_to_time(properties, frame_position, mlt_time_clock);
    if (s) {
        int h = 0, m = 0, sec = 0, msec = 0;
        sscanf(s, "%d:%d:%d.%d", &h, &m, &sec, &msec);
        fr_time = (h * 3600 + m * 60 + sec) * 1000 + msec;
    } else
        mlt_log_warning(filter,
                        "get_current_frame_time_ms time string null, giving up "
                        "[mlt_frame_original_position()=%d], retry result:%s\n",
                        frame_position,
                        mlt_properties_frames_to_time(properties, frame_position, mlt_time_clock));
    f_mutex.unlock();

    return file_time + fr_time * pdata->speed_multiplier;
}

//gets the nearest gps point [index] according to video time + input offset
int get_now_gpspoint_index(mlt_filter filter, mlt_frame frame, bool force_result /* = true */)
{
    private_data *pdata = (private_data *) filter->child;
    int64_t video_time_synced = get_current_frame_time_ms(filter, frame) + pdata->gps_offset;
    return binary_search_gps(filter_to_gps_data(filter), video_time_synced, force_result);
}

//returns the next gps point with a valid value for crt_source (starting with crt_i+1)
int get_next_valid_gpspoint_index(mlt_filter filter, int crt_i)
{
    private_data *pdata = (private_data *) filter->child;
    while (++crt_i < pdata->gps_points_size && get_crtval_bysrc(filter, crt_i) == GPS_UNINIT)
        ;
    //maybe TODO: add restriction for MAX_GPS_TIME? and allow depending on force_result
    return CLAMP(crt_i, 0, pdata->gps_points_size - 1);
}

//returns an interpolated gps point at the exact current video time + offset
gps_point_proc get_now_weighted_gpspoint(mlt_filter filter,
                                         mlt_frame frame,
                                         bool force_result /* = true */)
{
    private_data *pdata = (private_data *) filter->child;
    gps_point_proc crt;
    int i_now = -1, non_forced_i = -1;
    int max_gps_diff_ms = get_max_gps_diff_ms(filter_to_gps_data(filter));

    int64_t video_time_synced = get_current_frame_time_ms(filter, frame) + pdata->gps_offset;
    i_now = binary_search_gps(filter_to_gps_data(filter), video_time_synced, force_result);
    //make sure we don't mistakenly interpolate between gps[0] and gps[0+1] because the "forced" search returned gps[0] due to outside time
    non_forced_i = binary_search_gps(filter_to_gps_data(filter), video_time_synced, false);

    if (i_now == -1) //if force_result is true this will never happen
        return uninit_gps_proc_point;

    //interpolate if everything ok
    int next_i = get_next_valid_gpspoint_index(filter, i_now);
    if (non_forced_i != -1)
        crt = weighted_middle_point_proc(&pdata->gps_points_p[i_now],
                                         &pdata->gps_points_p[next_i],
                                         video_time_synced,
                                         max_gps_diff_ms);
    else
        crt = pdata->gps_points_p[i_now];
    return crt;
}

//initializes stuff and chooses which type of graph draw to call
static void draw_graphics(mlt_filter filter,
                          mlt_frame frame,
                          QImage *qimg,
                          int width,
                          int height,
                          s_base_crops &used_crops)
{
    private_data *pdata = (private_data *) filter->child;
    mlt_properties properties = MLT_FILTER_PROPERTIES(filter);
    QPainter p(qimg);

    prepare_canvas(filter, frame, qimg, p, width, height, used_crops);

    if (pdata->ui_crops.start_index < pdata->ui_crops.end_index) {
        //draw the main graph line
        if (pdata->graph_type == gpsg_2d_map_graph || pdata->graph_type == gpsg_crop_center_graph)
            draw_main_line_graph(filter, frame, p, used_crops);
        else if (pdata->graph_type == gpsg_speedometer_graph)
            draw_main_speedometer(filter, frame, p, used_crops);
    } else
        mlt_log_info(filter,
                     "min > max so nothing to print (index: start=%d,end=%d; start_p:%f,end_p:%f; "
                     "vertical: UIbot=%f,UItop=%f; horizontal: UIleft:%f,UIright:%f)\n",
                     pdata->ui_crops.start_index,
                     pdata->ui_crops.end_index,
                     mlt_properties_get_double(properties, "trim_start_p"),
                     mlt_properties_get_double(properties, "trim_end_p"),
                     pdata->ui_crops.bot,
                     pdata->ui_crops.top,
                     pdata->ui_crops.left,
                     pdata->ui_crops.right);

    p.end();
}

template<typename T>
static double get_as_percentage(T val, T min, T max)
{
    double ret = 1;
    if (max - min != 0)
        ret = (double) (val - min) / (max - min);
    return ret;
}

//computes used_crops from pdata->ui_crops (they change on each frame so can't store them in pdata)
static void process_frame_properties(mlt_filter filter, mlt_frame frame, s_base_crops &used_crops)
{
    private_data *pdata = (private_data *) filter->child;

    //if CROP_CENTER we'll compute used_crops from pdata->ui_crops every frame to keep the point in center
    if (pdata->graph_type == gpsg_crop_center_graph) {
        int i_now = get_now_gpspoint_index(filter, frame);
        gps_point_proc crt_weighted = get_now_weighted_gpspoint(filter, frame);

        if (get_crtval_bysrc(filter, i_now, 0, &crt_weighted) != GPS_UNINIT) {
            double now_perc_vertical
                = get_as_percentage(get_crtval_bysrc(filter, 0, 0, &crt_weighted),
                                    get_min_bysrc(filter),
                                    get_max_bysrc(filter));
            double now_perc_horizontal;
            if (pdata->graph_data_source == gspg_location_src)
                now_perc_horizontal = get_as_percentage(crt_weighted.lon,
                                                        get_min_bysrc(filter, gpsg_longitude_id),
                                                        get_max_bysrc(filter, gpsg_longitude_id));
            else
                now_perc_horizontal = get_as_percentage(crt_weighted.time,
                                                        pdata->ui_crops.min_crop_time,
                                                        pdata->ui_crops.max_crop_time);

            //final crop is now_percentage +/- half of 100 - user requested crop (so a 90% crop means show 10% of the map, 5% above and 5% below the centered now_point)
            double delta_h = (100 - pdata->ui_crops.left) / 2.0;
            used_crops.left = now_perc_horizontal * 100 - delta_h;
            used_crops.right = now_perc_horizontal * 100 + delta_h;

            double delta_v = (100 - pdata->ui_crops.bot) / 2.0;
            used_crops.bot = now_perc_vertical * 100 - delta_v;
            used_crops.top = now_perc_vertical * 100 + delta_v;

            //if not 2d map, center for vertical axis is not something we want, so just use standard crop logic
            if (pdata->graph_data_source != gspg_location_src) {
                used_crops.bot = pdata->ui_crops.bot;
                used_crops.top = pdata->ui_crops.top;
            }
        }
    } else /* --> not gpsg_crop_center_graph */
    {
        used_crops.bot = pdata->ui_crops.bot;
        used_crops.top = pdata->ui_crops.top;
        used_crops.left = pdata->ui_crops.left;
        used_crops.right = pdata->ui_crops.right;
    }
}

static int filter_get_image(mlt_frame frame,
                            uint8_t **image,
                            mlt_image_format *format,
                            int *width,
                            int *height,
                            int writable)
{
    int error = 0;
    mlt_filter filter = (mlt_filter) mlt_frame_pop_service(frame);
    s_base_crops used_crops = {0, 100, 0, 100};

    // Get the current image
    *format = choose_image_format(*format);
    error = mlt_frame_get_image(frame, image, format, width, height, writable);

    // Draw the graph
    if (!error) {
        process_frame_properties(filter, frame, used_crops);
        QImage qimg;
        convert_mlt_to_qimage(*image, &qimg, *width, *height, *format);
        draw_graphics(filter, frame, &qimg, *width, *height, used_crops);
        convert_qimage_to_mlt(&qimg, *image, *width, *height);
    } else
        mlt_log_warning(MLT_FILTER_SERVICE(filter),
                        "mlt_frame_get_image error=%d, can't draw at all\n",
                        error);
    return error;
}

//if crop mode is absolute we'll need to convert those values to a percentage as the rest of the code expects
static void convert_crop_values_to_percentages(mlt_filter filter, mlt_frame frame)
{
    private_data *pdata = (private_data *) filter->child;
    mlt_properties properties = MLT_FILTER_PROPERTIES(filter);
    int crop_mode_v = mlt_properties_get_int(properties, "crop_mode_v");
    int crop_mode_h = mlt_properties_get_int(properties, "crop_mode_h");

    if (crop_mode_h + crop_mode_v == 0) //0=already percentage, 1=need to convert
        return;

    double v_min = get_min_bysrc(filter);
    double v_max = get_max_bysrc(filter);
    v_min = convert_bysrc_to_format(filter, v_min);
    v_max = convert_bysrc_to_format(filter, v_max);

    //now calculate the percentages
    //resulted_crop_percentage = (input_absolute_value - crt_type_minimum_value) / (crt_type_maximum_value - crt_type_minimum_value) * 100
    if (crop_mode_v) {
        pdata->ui_crops.bot = (pdata->ui_crops.bot - v_min) / (v_max - v_min) * 100;
        pdata->ui_crops.top = (pdata->ui_crops.top - v_min) / (v_max - v_min) * 100;
    }
    //this is just to cover all cases, I doubt someone would manually input coordinates or unix time (in milliseconds) when they can just visually set the percentage
    if (crop_mode_h) {
        if (pdata->graph_data_source == gspg_location_src) //longitude
        {
            v_min = get_min_bysrc(filter, gpsg_longitude_id);
            v_max = get_max_bysrc(filter, gpsg_longitude_id);
            pdata->ui_crops.left = (pdata->ui_crops.left - v_min) / (v_max - v_min) * 100;
            pdata->ui_crops.right = (pdata->ui_crops.right - v_min) / (v_max - v_min) * 100;
        } else //time
        {
            v_min = pdata->ui_crops.min_crop_time;
            v_max = pdata->ui_crops.max_crop_time;
            pdata->ui_crops.left = (pdata->ui_crops.left - v_min) / (v_max - v_min) * 100;
            pdata->ui_crops.right = (pdata->ui_crops.right - v_min) / (v_max - v_min) * 100;
        }
    }
}

//goes through ALL gps data and finds min/max for gps, altitude, hr + computes map's aspect ratio and middle point
static void find_minmax_of_data(mlt_filter filter)
{
    private_data *pdata = (private_data *) filter->child;
    pdata->minmax.set_defaults();

#define assign_if_smaller(x, m) \
    if (x != GPS_UNINIT && x < m) \
        m = x;
#define assign_if_bigger(x, m) \
    if (x != GPS_UNINIT && x > m) \
        m = x;
    for (int i = 0; i < pdata->gps_points_size; i++) {
        gps_point_proc *crt = &pdata->gps_points_p[i];
        assign_if_smaller(crt->lat, pdata->minmax.min_lat);
        assign_if_bigger(crt->lat, pdata->minmax.max_lat);
        assign_if_smaller(crt->lon, pdata->minmax.min_lon);
        assign_if_bigger(crt->lon, pdata->minmax.max_lon);
        assign_if_smaller(crt->ele, pdata->minmax.min_ele);
        assign_if_bigger(crt->ele, pdata->minmax.max_ele);
        assign_if_smaller(crt->speed, pdata->minmax.min_speed);
        assign_if_bigger(crt->speed, pdata->minmax.max_speed);
        assign_if_smaller(crt->hr, pdata->minmax.min_hr);
        assign_if_bigger(crt->hr, pdata->minmax.max_hr);
        assign_if_smaller(crt->grade_p, pdata->minmax.min_grade_p);
        assign_if_bigger(crt->grade_p, pdata->minmax.max_grade_p);
    }
    pdata->minmax.min_lat_projected = project_latitude(pdata->minmax.min_lat);
    pdata->minmax.max_lat_projected = project_latitude(pdata->minmax.max_lat);
#undef assign_if_smaller
#undef assign_if_bigger

    //compute the gps track aspect ratio (from coords)
    double map_aspect_ratio = 1;
    double map_width = pdata->minmax.max_lon - pdata->minmax.min_lon;
    double map_height = pdata->minmax.max_lat_projected - pdata->minmax.min_lat_projected;
    if (map_width && map_height) {
        map_aspect_ratio = map_width / map_height;
        pdata->map_aspect_ratio_from_distance = map_aspect_ratio;
    }
    mlt_properties_set_double(MLT_FILTER_PROPERTIES(filter),
                              "map_original_aspect_ratio",
                              map_aspect_ratio);

    char middle_point[255];
    double middle_lat = (pdata->minmax.min_lat + pdata->minmax.max_lat) / 2;
    double middle_lon = (pdata->minmax.min_lon + pdata->minmax.max_lon) / 2;
    snprintf(middle_point, 255, "%.6f, %.6f", middle_lat, swap_180_if_needed(middle_lon));
    mlt_properties_set(MLT_FILTER_PROPERTIES(filter), "map_coords_hint", middle_point);

    mlt_log_info(filter,
                 "gps file [%d points] minmax: min_lat,lon-max_lat,lon: %f,%f:%f,%f; ele: %f,%f; "
                 "speed:%f,%f; "
                 "hr:%f,%f; grade_p:%f,%f%%, map_ar:%f, mid_point:%s \n",
                 pdata->gps_points_size,
                 pdata->minmax.min_lat,
                 pdata->minmax.min_lon,
                 pdata->minmax.max_lat,
                 pdata->minmax.max_lon,
                 pdata->minmax.min_ele,
                 pdata->minmax.max_ele,
                 pdata->minmax.min_speed,
                 pdata->minmax.max_speed,
                 pdata->minmax.min_hr,
                 pdata->minmax.max_hr,
                 pdata->minmax.min_grade_p,
                 pdata->minmax.max_grade_p,
                 map_aspect_ratio,
                 middle_point);
}

// Reads and updates all necessary filter properties, and smooths+processes the gps data if needed
static void process_filter_properties(mlt_filter filter, mlt_frame frame)
{
    mlt_properties properties = MLT_FILTER_PROPERTIES(filter);
    private_data *pdata = (private_data *) filter->child;
    char do_smoothing = 0;

    //read properties
    int read_video_offset1 = mlt_properties_get_int(properties, "time_offset");
    int read_smooth_val = mlt_properties_get_int(properties, "smoothing_value");
    double read_speed_multiplier = mlt_properties_get_double(properties, "speed_multiplier");
    int64_t original_video_time = get_original_video_file_time_mseconds(frame);

    //process properties
    pdata->gps_offset = (int64_t) read_video_offset1 * 1000;
    pdata->speed_multiplier = (read_speed_multiplier ? read_speed_multiplier : 1);
    if (pdata->last_smooth_lvl != read_smooth_val) {
        pdata->last_smooth_lvl = read_smooth_val;
        do_smoothing = 1;
    }

    char video_start_text[255], gps_start_text[255];
    mseconds_to_timestring(original_video_time, NULL, video_start_text);
    mseconds_to_timestring(pdata->first_gps_time, NULL, gps_start_text);

    if (do_smoothing) { //also always does processing + updates minmax
        process_gps_smoothing(filter_to_gps_data(filter), 1);
        find_minmax_of_data(filter);
    }

    pdata->graph_data_source = mlt_properties_get_int(properties, "graph_data_source");
    pdata->graph_type = mlt_properties_get_int(properties, "graph_type");

    pdata->ui_crops.trim_start_p = mlt_properties_get_double(properties, "trim_start_p");
    pdata->ui_crops.start_index = pdata->gps_points_size / 100.0 * pdata->ui_crops.trim_start_p;
    pdata->ui_crops.start_index = CLAMP(pdata->ui_crops.start_index, 0, pdata->gps_points_size - 1);
    pdata->ui_crops.min_crop_time = pdata->gps_points_p[pdata->ui_crops.start_index].time;

    pdata->ui_crops.trim_end_p = mlt_properties_get_double(properties, "trim_end_p");
    pdata->ui_crops.end_index = pdata->gps_points_size / 100.0 * pdata->ui_crops.trim_end_p;
    pdata->ui_crops.end_index = CLAMP(pdata->ui_crops.end_index, 0, pdata->gps_points_size - 1);
    pdata->ui_crops.max_crop_time = pdata->gps_points_p[pdata->ui_crops.end_index].time;

    pdata->ui_crops.bot = mlt_properties_get_double(properties, "crop_bot_p");
    pdata->ui_crops.top = mlt_properties_get_double(properties, "crop_top_p");
    pdata->ui_crops.left = mlt_properties_get_double(properties, "crop_left_p");
    pdata->ui_crops.right = mlt_properties_get_double(properties, "crop_right_p");

    convert_crop_values_to_percentages(filter, frame);

    //(re-)read background img file if path changed
    char *bg_path = mlt_properties_get(properties, "bg_img_path");
    bool new_img_loaded = false;
    if (bg_path && strlen(bg_path) > 0 && bg_path[0] != '!') {
        if (strcmp(bg_path, pdata->last_bg_img_path)) {
            if (pdata->bg_img.load(bg_path)) {
                pdata->bg_img = pdata->bg_img.convertToFormat(QImage::Format_ARGB32);
                strncpy(pdata->last_bg_img_path, bg_path, 255);
                new_img_loaded = true;
            } else {
                mlt_log_warning(filter, "Failed to load background image: %s\n", bg_path);
            }
        }
    } else //delete the previous image
    {
        if (strlen(pdata->last_bg_img_path) > 0) {
            pdata->last_bg_img_path[0] = 0;
            pdata->bg_img = QImage();
            pdata->bg_img_scaled = QImage();
        }
    }

    //process the image so it matches the map when overlaid
    //-> image center must equal gps track center
    //-> unless image size is already perfectly matching track min/max, we'll need user input to re-scale it
    double bg_scale_w = mlt_properties_get_double(properties, "bg_scale_w");
    if (!pdata->bg_img.isNull()
        && (new_img_loaded
            || bg_scale_w != pdata->bg_img_scaled_width)) //only update if something changed
    {
        double map_ar
            = pdata->map_aspect_ratio_from_distance; //mlt_properties_get_double(properties, "map_original_aspect_ratio");
        double bg_ar = (double) pdata->bg_img.width() / pdata->bg_img.height();
        double bg_scale_h = bg_scale_w * bg_ar / map_ar;
        pdata->bg_img_scaled_width = bg_scale_w;
        pdata->bg_img_scaled_height = bg_scale_h;
        // mlt_log_info(filter, "scale w=%f -> h=%f; bg_ar=%f, map_ar=%f", bg_scale_w, bg_scale_h, bg_ar, map_ar);

        //step 1) scale image width or height by map_aspect_ratio so now they both have the same aspect_ratio
        double new_bg_width = pdata->bg_img.width();
        double new_bg_height = pdata->bg_img.height();
        if (map_ar > 1)
            new_bg_width *= map_ar;
        else
            new_bg_height /= map_ar;
        pdata->bg_img_scaled = pdata->bg_img.scaled(new_bg_width,
                                                    new_bg_height,
                                                    Qt::IgnoreAspectRatio,
                                                    Qt::SmoothTransformation);
        // mlt_log_info(filter, "scaled image= x,y w,h= %d,%d %d,%d\n", pdata->bg_img_scaled.rect().x(), pdata->bg_img_scaled.rect().y(), pdata->bg_img_scaled.rect().width(), pdata->bg_img_scaled.rect().height());
        // pdata->bg_img_scaled.save("qimage_saved02_scaled_by_mapAR.jpg");

        //next steps are in prepare_canvas() as they depend on each frame position (used_crops)
    }

    //write properties
    mlt_properties_set(properties, "gps_start_text", gps_start_text);
    mlt_properties_set(properties, "video_start_text", video_start_text);
    mlt_properties_set_int(properties,
                           "auto_gps_offset_start",
                           (pdata->first_gps_time - original_video_time) / 1000);
    mlt_properties_set_int(properties,
                           "auto_gps_offset_now",
                           (pdata->first_gps_time - get_current_frame_time_ms(filter, frame))
                               / 1000);
}

//checks if a new file is selected and if so, does private_data cleanup and calls qxml_parse_file
//+ computes some one time pdata values
static void process_file(mlt_filter filter, mlt_frame frame)
{
    private_data *pdata = (private_data *) filter->child;
    mlt_properties properties = MLT_FILTER_PROPERTIES(filter);
    char *filename = mlt_properties_get(properties, "resource");
    bool guess_offset = (mlt_properties_get_int(properties, "time_offset") == 0)
                        && (strlen(pdata->last_filename) == 0);

    //if there's no file selected just return
    if (!filename || !strcmp(filename, ""))
        return;

    //check if the file has been changed, if not, current data is ok, do nothing
    if (strcmp(pdata->last_filename, filename)) {
        // mlt_log_info(filter, "Reading new file: last_filename (%s) != entered_filename (%s), swap_180 = %d\n", pdata->last_filename, filename, swap);
        default_priv_data(pdata);
        strcpy(pdata->last_filename, filename);

        if (qxml_parse_file(filter_to_gps_data(filter)) == 1) {
            get_first_gps_time(filter_to_gps_data(filter));
            get_last_gps_time(filter_to_gps_data(filter));

            //when loading the first file, sync gps start with video start
            int64_t original_video_time = get_original_video_file_time_mseconds(frame);
            if (guess_offset) {
                pdata->gps_offset = pdata->first_gps_time - original_video_time;
                mlt_properties_set_int(properties, "time_offset", pdata->gps_offset / 1000);
            }

            //assume smooth is 5 (default) so we can guarantee *gps_points_p and min/max allocation
            pdata->last_smooth_lvl = 5;
            process_gps_smoothing(filter_to_gps_data(filter), 1);
            find_minmax_of_data(filter);
        } else {
            default_priv_data(pdata);
            //store name in pdata or it'll retry reading every frame if bad file
            strcpy(pdata->last_filename, filename);
        }
    }
}

/** Filter processing.
*/
static mlt_frame filter_process(mlt_filter filter, mlt_frame frame)
{
    private_data *pdata = (private_data *) filter->child;

    process_file(filter, frame);
    if (pdata->gps_points_r == NULL || pdata->gps_points_size == 0) {
        return frame;
    }
    process_filter_properties(filter, frame);

    mlt_frame_push_service(frame, filter);
    mlt_frame_push_get_image(frame, filter_get_image);
    return frame;
}

static void filter_close(mlt_filter filter)
{
    private_data *pdata = (private_data *) filter->child;

    default_priv_data(pdata);
    free(pdata);

    filter->child = NULL;
    filter->close = NULL;
    filter->parent.close = NULL;
    mlt_service_close(&filter->parent);
}

/** Constructor for the filter.
*/
extern "C" {
mlt_filter filter_gpsgraphic_init(mlt_profile profile,
                                  mlt_service_type type,
                                  const char *id,
                                  char *arg)
{
    mlt_filter filter = mlt_filter_new();
    private_data *pdata = (private_data *) calloc(1, sizeof(private_data));

    if (filter && pdata && createQApplicationIfNeeded(MLT_FILTER_SERVICE(filter))) {
        mlt_properties properties = MLT_FILTER_PROPERTIES(filter);
        mlt_properties_set_string(properties, "resource", arg);
        //sync stuff
        mlt_properties_set_int(properties, "time_offset", 0);
        mlt_properties_set_int(properties, "smoothing_value", 5);
        mlt_properties_set_double(properties, "speed_multiplier", 1);
        //graph data stuff
        mlt_properties_set_int(properties, "graph_data_source", 2);
        mlt_properties_set_int(properties, "graph_type", 0);
        mlt_properties_set_double(properties, "trim_start_p", 0);
        mlt_properties_set_double(properties, "trim_end_p", 100);
        mlt_properties_set_int(properties, "crop_mode_h", 0);
        mlt_properties_set_double(properties, "crop_left_p", 0);
        mlt_properties_set_double(properties, "crop_right_p", 100);
        mlt_properties_set_int(properties, "crop_mode_v", 0);
        mlt_properties_set_double(properties, "crop_bot_p", 0);
        mlt_properties_set_double(properties, "crop_top_p", 100);
        //graph design stuff
        mlt_properties_set_int(properties, "color_style", 1);
        mlt_properties_set(properties, "color.1", "#ff00aaff");
        mlt_properties_set(properties, "color.2", "#ff00e000");
        mlt_properties_set(properties, "color.3", "#ffffff00");
        mlt_properties_set(properties, "color.4", "#ffff8c00");
        mlt_properties_set(properties, "color.5", "#ffff0000");
        mlt_properties_set_int(properties, "show_now_dot", 1);
        mlt_properties_set(properties, "now_dot_color", "#00ffffff");
        mlt_properties_set_int(properties, "show_now_text", 1);
        mlt_properties_set_double(properties, "angle", 0);
        mlt_properties_set_int(properties, "thickness", 5);
        mlt_properties_set(properties, "rect", "10% 10% 30% 30%");
        mlt_properties_set_int(properties, "show_grid", 0);
        mlt_properties_set(properties, "legend_unit", "");
        mlt_properties_set_int(properties, "draw_individual_dots", 0);
        //background image
        mlt_properties_set(properties, "map_coords_hint", "<no location file processed>");
        mlt_properties_set(properties, "bg_img_path", "");
        mlt_properties_set_double(properties, "bg_scale_w", 1);
        mlt_properties_set_double(properties, "bg_opacity", 1);

        filter->close = filter_close;
        filter->process = filter_process;
        filter->child = pdata;
    } else {
        mlt_log_error(MLT_FILTER_SERVICE(filter), "Filter gps graphic failed\n");

        if (filter) {
            mlt_filter_close(filter);
        }

        if (pdata) {
            free(pdata);
        }

        filter = NULL;
    }
    return filter;
}
}
