# Copyright (c) 2021-2024 Andy Maleh
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

require 'glimmer/libui/control_proxy'
require 'glimmer/libui/control_proxy/area_proxy'
require 'glimmer/libui/parent'
require 'glimmer/libui/control_proxy/transformable'
require 'glimmer/libui/perfect_shaped'

module Glimmer
  module LibUI
    class ControlProxy
      # Proxy for LibUI path objects
      #
      # Follows the Proxy Design Pattern
      class PathProxy < ControlProxy
        include Parent
        include PerfectShaped
        prepend Transformable
      
        def initialize(keyword, parent, args, &block)
          @keyword = keyword
          @parent_proxy = parent
          @args = args
          @block = block
          post_add_content if @block.nil?
        end
      
        def post_add_content
          super
          if @parent_proxy.nil? && AreaProxy.current_area_draw_params
            draw(AreaProxy.current_area_draw_params)
            destroy
          end
        end
        
        def draw(area_draw_params)
          build_control
          children.dup.each {|child| child.draw(area_draw_params)}
          ::LibUI.draw_path_end(@libui)
          ::LibUI.draw_fill(area_draw_params[:context], @libui, fill_draw_brush.to_ptr) unless fill.empty?
          ::LibUI.draw_stroke(area_draw_params[:context], @libui, stroke_draw_brush, draw_stroke_params) unless stroke.empty?
          ::LibUI.draw_free_path(@libui)
        end
        
        def draw_fill_mode
          @args[0].is_a?(Integer) ? (@args[0] == 0 ? :winding : :alternate ) : ((@args[0].is_a?(String) || @args[0].is_a?(Symbol)) ? @args[0].to_sym : :winding)
        end
        
        def draw_fill_mode_value
          @args[0].is_a?(Integer) ? @args[0] : @args[0].to_s == 'alternate' ? 1 : 0
        end
        
        def fill(*args)
          args = args.first if args.size == 1 && (args.first.is_a?(Array) || args.first.is_a?(Hash) || args.first.is_a?(String) || args.first.is_a?(Symbol))
          if args.empty?
            @fill ||= {}
          else
            new_color = Glimmer::LibUI.interpret_color(args)
            if new_color != @fill
              # TODO consider replacing unobserve with observer_registration.deregister
              @fill_observer&.unobserve(@fill, attribute_writer_type: [:attribute=, :set_attribute]) if @fill
              @fill = new_color
              request_auto_redraw
            end
          end
          @fill.tap do
            @fill_observer ||= Glimmer::DataBinding::Observer.proc do
              request_auto_redraw
            end
            @fill_observer.observe(@fill, attribute_writer_type: [:attribute=, :set_attribute])
          end
        end
        alias fill= fill
        alias set_fill fill
        
        def fill_draw_brush
          @fill_draw_brush ||= ::LibUI::FFI::DrawBrush.malloc
          init_draw_brush(@fill_draw_brush, @fill)
          @fill_draw_brush
        end
      
        def stroke(*args)
          args = args.first if args.size == 1 && (args.first.is_a?(Array) || args.first.is_a?(Hash) || args.first.is_a?(String) || args.first.is_a?(Symbol))
          if args.empty?
            @stroke ||= {}
          else
            new_color = Glimmer::LibUI.interpret_color(args)
            if new_color != @stroke
              # TODO consider replacing unobserve with observer_registration.deregister
              @stroke_observer&.unobserve(@stroke, attribute_writer_type: [:attribute=, :set_attribute]) if @stroke
              @stroke = Glimmer::LibUI.interpret_color(args)
              request_auto_redraw
            end
          end
          @stroke.tap do
            @stroke_observer ||= Glimmer::DataBinding::Observer.proc do
              request_auto_redraw
            end
            @stroke_observer.observe(@stroke, attribute_writer_type: [:attribute=, :set_attribute])
          end
        end
        alias stroke= stroke
        alias set_stroke stroke
        
        def stroke_draw_brush
          @stroke_draw_brush ||= ::LibUI::FFI::DrawBrush.malloc
          init_draw_brush(@stroke_draw_brush, @stroke)
          @stroke_draw_brush
        end
        
        def draw_stroke_params
          @draw_stroke_params ||= ::LibUI::FFI::DrawStrokeParams.malloc
          @draw_stroke_params.Cap = draw_line_cap # flat
          @draw_stroke_params.Join = draw_line_join # miter
          @draw_stroke_params.Thickness = @stroke[:thickness] || 1
          @draw_stroke_params.MiterLimit = @stroke[:miter_limit] || 10 # DEFAULT_MITER_LIMIT
          @draw_stroke_params.Dashes = @stroke[:dashes].to_a.pack('d*')
          @draw_stroke_params.NumDashes = @stroke[:dashes].to_a.count
          @draw_stroke_params.DashPhase = @stroke[:dash_phase] || 0
          @draw_stroke_params
        end
        
        def draw_line_cap
          Glimmer::LibUI.enum_symbol_to_value(:draw_line_cap, @stroke && @stroke[:cap])
        end
        
        def draw_line_join
          Glimmer::LibUI.enum_symbol_to_value(:draw_line_join, @stroke && @stroke[:join])
        end
        
        def destroy
          return if ControlProxy.main_window_proxy&.destroying?
          deregister_all_custom_listeners
          @parent_proxy&.children&.delete(self)
          ControlProxy.control_proxies.delete(self)
        end
        
        def redraw
          @parent_proxy&.redraw
        end
        
        def request_auto_redraw
          @parent_proxy&.request_auto_redraw
        end
        
        def move_by(x_delta, y_delta)
          children.each {|child| child.move_by(x_delta, y_delta)}
        end
        
        def perfect_shape
          require 'perfect-shape'
          the_perfect_shape_dependencies = perfect_shape_dependencies
          if the_perfect_shape_dependencies != @perfect_shape_dependencies
            draw_fill_mode, _ = @perfect_shape_dependencies = the_perfect_shape_dependencies
            shapes = children.map(&:perfect_shape)
            new_shapes = []
            shapes.each do |shape|
              if shape.is_a?(PerfectShape::Path)
                new_shapes += shape.basic_shapes
              else
                new_shapes << shape
              end
            end
            shapes = new_shapes
            winding_rule = draw_fill_mode == :winding ? :wind_non_zero : :wind_even_odd
            @perfect_shape = PerfectShape::Path.new(
              shapes: shapes,
              winding_rule: winding_rule,
              line_to_complex_shapes: true
            )
          end
          @perfect_shape
        end
        
        def perfect_shape_dependencies
          [draw_fill_mode, children.map(&:perfect_shape_dependencies)]
        end
        
        private
        
        def build_control
          @libui = ::LibUI.draw_new_path(draw_fill_mode_value)
        end
        
        def init_draw_brush(draw_brush, draw_brush_args)
          if draw_brush_args[:r] || draw_brush_args[:g] || draw_brush_args[:b] || draw_brush_args[:a]
            draw_brush_args[:type] ||= :solid
          elsif draw_brush_args[:outer_radius]
            draw_brush_args[:type] ||= :radial_gradient
          else
            draw_brush_args[:type] ||= :linear_gradient
          end
          draw_brush.Type = Glimmer::LibUI.enum_symbol_to_value(:draw_brush_type, draw_brush_args[:type])
          if draw_brush.Type == 0
            draw_brush.R = (draw_brush_args[:r] || draw_brush_args[:red]).to_f / 255.0
            draw_brush.G = (draw_brush_args[:g] || draw_brush_args[:green]).to_f / 255.0
            draw_brush.B = (draw_brush_args[:b] || draw_brush_args[:blue]).to_f / 255.0
            draw_brush.A = (draw_brush_args[:a] || draw_brush_args[:alpha] || 1.0)
          else
            draw_brush.X0 = draw_brush_args[:x0].to_f
            draw_brush.Y0 = draw_brush_args[:y0].to_f
            draw_brush.X1 = draw_brush_args[:x1].to_f
            draw_brush.Y1 = draw_brush_args[:y1].to_f
            draw_brush.OuterRadius = draw_brush_args[:outer_radius].to_f if draw_brush.Type == 2
            stop_structs = draw_brush_args[:stops].to_a.map do |stop|
              ::LibUI::FFI::DrawBrushGradientStop.malloc.tap do |stop_struct|
                stop_struct.Pos = stop[:pos].to_f
                stop_color = Glimmer::LibUI.interpret_color(stop)
                stop_struct.R = stop_color[:r].to_f / 255.0
                stop_struct.G = stop_color[:g].to_f / 255.0
                stop_struct.B = stop_color[:b].to_f / 255.0
                stop_struct.A = stop_color[:a] || 1.0
              end
            end
            draw_brush.NumStops = stop_structs.count
            draw_brush.Stops = stop_structs.map(&:to_ptr).map(&:to_s).reduce(:+)
          end
        end
      end
    end
  end
end
