# frozen_string_literal: true

require 'json'

module GPX
  # Class to parse GeoJSON LineStrings, MultiLineStrings, Points,
  # and MultiPoint geometric objects to GPX format. For the full
  # specification of GeoJSON, see:
  #   http://geojson.org/geojson-spec.html
  # Note that GeoJSON coordinates are specified in lon/lat format,
  # instead of the more traditional lat/lon format.
  #
  class GeoJSON
    class << self
      FEATURE = 'Feature'
      LINESTRING = 'LineString'
      MULTILINESTRING = 'MultiLineString'
      POINT = 'Point'
      MULTIPOINT = 'MultiPoint'

      # Conversion can be initiated by either specifying a file,
      # file name, or by passing in GeoJSON data as a string.
      # Examples:
      #   GPX::GeoJSON.convert_to_gpx(geojson_file: 'mygeojsonfile.json')
      # or
      #   file = File.new('mygeojsonfile.json', 'r')
      #   GPX::GeoJSON.convert_to_gpx(geojson_file: file)
      # or
      #   data = JSON.generate(my_geojson_hash)
      #   GPX::GeoJSON.convert_to_gpx(geojson_data: data)
      #
      # Returns a GPX::GPX_File object populated with the converted data.
      #
      def convert_to_gpx(opts = {})
        geojson = geojson_data_from(opts)
        gpx_file = GPX::GPXFile.new
        add_tracks_to(gpx_file, geojson, opts)
        add_waypoints_to(gpx_file, geojson, opts)
        gpx_file
      end

      private

      def geojson_data_from(opts)
        if opts[:geojson_file]
          parse_geojson_data_from_file(opts[:geojson_file])
        elsif opts[:geojson_data]
          parse_geojson_data(opts[:geojson_data])
        else
          raise ArgumentError,
                'Must pass value for \':geojson_file\' ' \
                'or \':geojson_data\' to convert_to_gpx'
        end
      end

      def parse_geojson_data_from_file(filename)
        parse_geojson_data(IO.read(filename))
      end

      def parse_geojson_data(data)
        JSON.parse(data)
      end

      def add_tracks_to(gpx_file, geojson, opts)
        tracks = [line_strings_to_track(geojson, opts)] +
                 multi_line_strings_to_tracks(geojson, opts)
        tracks.compact!
        gpx_file.tracks += tracks
        gpx_file.tracks.each { |t| gpx_file.update_meta_data(t) }
      end

      def add_waypoints_to(gpx_file, geojson, opts)
        gpx_file.waypoints +=
          points_to_waypoints(geojson, gpx_file, opts) +
          multi_points_to_waypoints(geojson, gpx_file, opts)
      end

      # Converts GeoJSON 'LineString' features.
      # Current strategy is to convert each LineString into a
      # Track Segment, returning a Track for all LineStrings.
      #
      def line_strings_to_track(geojson, opts)
        line_strings = line_strings_in(geojson)
        return nil unless line_strings.any?

        track = GPX::Track.new
        line_strings.each do |ls|
          coords = ls['geometry']['coordinates']
          segment = coords_to_segment(coords)

          opts[:line_string_feature_to_segment]&.call(ls, segment)
          track.append_segment(segment)
        end
        track
      end

      # Converts GeoJSON 'MultiLineString' features.
      # Current strategy is to convert each MultiLineString
      # into a Track, with each set of LineString coordinates
      # within a MultiLineString a Track Segment.
      #
      def multi_line_strings_to_tracks(geojson, opts)
        tracks = []
        multi_line_strings_in(geojson).each do |mls|
          track = GPX::Track.new
          mls['geometry']['coordinates'].each do |coords|
            seg = coords_to_segment(coords)
            seg.track = track

            track.append_segment(seg)
          end

          opts[:multi_line_string_feature_to_track]&.call(mls, track)
          tracks << track
        end
        tracks
      end

      # Converts GeoJSON 'Point' features.
      # Current strategy is to convert each Point
      # feature into a GPX waypoint.
      #
      def points_to_waypoints(geojson, gpx_file, opts)
        points_in(geojson).reduce([]) do |acc, pt|
          coords = pt['geometry']['coordinates']
          waypoint = point_to_waypoint(coords, gpx_file)

          opts[:point_feature_to_waypoint]&.call(pt, waypoint)

          acc << waypoint
        end
      end

      # Converts GeoJSON 'MultiPoint' features.
      # Current strategy is to convert each coordinate
      # point in a MultiPoint to a GPX waypoint.
      #
      # NOTE: It is debatable that a MultiPoint feature
      # might translate best into a GPX route, which is
      # described as
      #   "an ordered list of waypoints representing a
      #    series of turn points leading to a destination."
      # See http://www.topografix.com/gpx/1/1/#type_rteType
      #
      def multi_points_to_waypoints(geojson, gpx_file, opts)
        multi_points_in(geojson).each_with_object([]) do |mpt, acc|
          mpt['geometry']['coordinates'].each do |coords|
            waypoint = point_to_waypoint(coords, gpx_file)

            opts[:multi_point_feature_to_waypoint]&.call(mpt, waypoint)

            acc << waypoint
          end
        end
      end

      # Given an array of [lng, lat, ele] coordinates,
      # return a GPX track segment.
      #
      def coords_to_segment(coords)
        seg = GPX::Segment.new
        coords.each do |pt|
          seg.append_point(point_to_track_point(pt, seg))
        end
        seg
      end

      # Given a GeoJSON coordinate point, return
      # a GPX::Waypoint
      def point_to_waypoint(point, gpx_file)
        GPX::Waypoint.new(gpx_file: gpx_file,
                          lon: point[0],
                          lat: point[1],
                          elevation: point[2])
      end

      # Given a GeoJSON coorindate point, and
      # GPX segment, return a GPX::TrackPoint.
      #
      def point_to_track_point(point, seg)
        GPX::TrackPoint.new(segment: seg,
                            lon: point[0],
                            lat: point[1],
                            elevation: point[2])
      end

      # Returns all features in the passed geojson
      # that match the type.
      #
      def features_for(geojson, type)
        geojson['features'].find_all do |f|
          f['type'] == FEATURE && f['geometry']['type'] == type
        end
      end

      def points_in(geojson)
        features_for(geojson, POINT)
      end

      def multi_points_in(geojson)
        features_for(geojson, MULTIPOINT)
      end

      def line_strings_in(geojson)
        features_for(geojson, LINESTRING)
      end

      def multi_line_strings_in(geojson)
        features_for(geojson, MULTILINESTRING)
      end
    end
  end
end
