# backtick_javascript: true

# Copyright (c) 2023-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/web/listener_proxy'

module Glimmer
  module Web
    class ElementProxy
      class << self
        def keyword_supported?(keyword)
          ELEMENT_KEYWORDS.include?(keyword.to_s)
        end
      
        # NOTE: Avoid using this method until we start supporting ElementProxy subclasses
        # in which case, we must cache them to avoid the slow performance of element_type
        # Factory Method that translates a Glimmer DSL keyword into a ElementProxy object
        def for(keyword, parent, args, block)
          element_type(keyword).new(keyword, parent, args, block)
        end
        
        # NOTE: Avoid using this method for now as it has slow performance
        # returns Ruby proxy class (type) that would handle this keyword
        def element_type(keyword)
          class_name_main = "#{keyword.camelcase(:upper)}Proxy"
          Glimmer::Web::ElementProxy.const_get(class_name_main.to_sym)
        rescue NameError => e
          Glimmer::Web::ElementProxy
        end
        
        def next_id_number_for(name)
          @max_id_numbers[name] = max_id_number_for(name) + 1
        end
        
        def max_id_number_for(name)
          @max_id_numbers[name] = max_id_numbers[name] || 0
        end
        
        def max_id_numbers
          @max_id_numbers ||= reset_max_id_numbers!
        end
        
        def reset_max_id_numbers!
          @max_id_numbers = {}
        end
        
        def underscored_widget_name(widget_proxy)
          widget_proxy.class.name.split(/::|\./).last.sub(/Proxy$/, '').underscore
        end
        
        def widget_handling_listener
          @@widget_handling_listener
        end
        
        def render_html(element, attributes, content = nil)
          attributes = attributes.reduce('') do |output, option_pair|
            attribute, value = option_pair
            value = value.to_s.sub('"', '&quot;').sub("'", '&apos;')
            output += " #{attribute}=\"#{value}\""
          end
          if content.nil?
            "<#{element}#{attributes} />"
          else
            "<#{element}#{attributes}>#{content}</#{element}>"
          end
        end
        
        def unrendered_dom_element(keyword)
          @unrendered_dom_elements ||= {}
          @unrendered_dom_elements[keyword] ||= Element["<#{keyword} />"]
        end
      end
      
      include Glimmer
      
      Event = Struct.new(:widget, keyword_init: true)
      
      ELEMENT_KEYWORDS = [
        "a", "abbr", "acronym", "address", "applet", "area", "article", "aside", "audio",
        "base", "basefont", "bdi", "bdo", "bgsound", "big", "blink", "blockquote", "body",
        "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "data",
        "datalist", "dd", "decorator", "details", "dfn", "dir", "div", "dl", "dt",
        "element", "embed", "fieldset", "figcaption", "figure", "font", "footer", "form", "frame",
        "frameset", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup",
        "hr", "html", "iframe", "img", "input", "isindex", "kbd", "keygen",
        "label", "legend", "li", "link", "listing", "main", "map", "marquee", "menu",
        "menuitem", "meta", "meter", "nav", "nobr", "noframes", "noscript", "object", "ol", "optgroup",
        "option", "output", "p", "param", "plaintext", "pre", "progress", "q", "rp", "rt",
        "ruby", "s", "samp", "script", "section", "select", "shadow", "source", "spacer",
        "span", "strike", "style", "summary", "table", "tbody", "td",
        "template", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "tt",
        "u", "ul", "var", "video", "wbr", "xmp",
      ]

      GLIMMER_ATTRIBUTES = [:parent]
      PROPERTY_ALIASES = {
        'inner_html' => 'innerHTML',
        'outer_html' => 'outerHTML',
      }
      FORMAT_DATETIME = '%Y-%m-%dT%H:%M'
      FORMAT_DATE = '%Y-%m-%d'
      FORMAT_TIME = '%H:%M'
      REGEX_FORMAT_DATETIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/
      REGEX_FORMAT_DATE = /^\d{4}-\d{2}-\d{2}$/
      REGEX_FORMAT_TIME = /^\d{2}:\d{2}$/
      
      attr_reader :keyword, :parent, :parent_component, :args, :options, :children, :enabled, :foreground, :background, :removed?, :rendered
      alias rendered? rendered
      
      def initialize(keyword, parent, args, block)
        @keyword = keyword
        @parent = parent.is_a?(Glimmer::Web::Component) ? parent.markup_root : parent
        @parent_component = parent if parent.is_a?(Glimmer::Web::Component)
        @options = args.last.is_a?(Hash) ? args.last.symbolize_keys : {}
        if parent.nil?
          options[:parent] ||= Component.interpretation_stack.last&.options&.[](:parent)
          options[:render] ||= Component.interpretation_stack.last&.options&.[](:render)
          options[:bulk_render] ||= Component.interpretation_stack.last&.options&.[](:bulk_render)
        end
        @args = args
        @block = block
        @children = []
        @parent&.post_initialize_child(self)
        render if !bulk_render? && !@rendered && render_after_create?
      end
      
      def bulk_render?
        options[:bulk_render] != false && (@parent.nil? || @parent.bulk_render?)
      end
      
      def render_after_create?
        options[:render] != false && (@parent.nil? || @parent.render_after_create?)
      end
      
      # Executes for the parent of a child that just got added
      def post_initialize_child(child)
        @children << child
        child.render if !bulk_render? && !render_after_create?
      end
      
      # Executes for the parent of a child that just got removed
      def post_remove_child(child)
        @children.delete(child)
      end
      
      # Executes at the closing of a parent widget curly braces after all children/properties have been added/set
      def post_add_content
        render if bulk_render? && @parent.nil?
      end
      
      def css_classes
        dom_element.attr('class').to_s.split if rendered?
      end
      
      def remove
        on_remove_listeners = listeners_for('on_remove').dup
        if rendered?
          @children.dup.each do |child|
            child.remove
          end
          remove_all_listeners
          dom_element.remove
        end
        parent&.post_remove_child(self)
        @removed = true
        on_remove_listeners.each do |listener|
          listener.original_event_listener.call(EventProxy.new(listener: listener))
        end
      end
      
      def remove_all_listeners
        listeners.each do |event, event_listeners|
          event_listeners.dup.each(&:unregister)
        end
        listeners.clear
        data_bindings.each do |element_binding, model_binding|
          element_binding.unregister_all_observables
        end
        data_bindings.clear
      end
      
      # Subclasses can override with their own selector
      def selector
        ".#{element_id}"
      end

      # Root element representing widget. Must be overridden by subclasses if different from div
      def element
        keyword
      end
      
      def shell
        current_widget = self
        current_widget = current_widget.parent until current_widget.parent.nil?
        current_widget
      end

      def parents
        parents_array = []
        current_widget = self
        until current_widget.parent.nil?
          current_widget = current_widget.parent
          parents_array << current_widget
        end
        parents_array
      end

      def dialog_ancestor
        parents.detect {|p| p.is_a?(DialogProxy)}
      end
      
      def print
        `window.print()`
        true
      end

      def enabled=(value)
        if rendered?
          @enabled = value
          dom_element.prop('disabled', !@enabled)
        else
          enqueue_post_render_method_call('enabled=', value)
        end
      end
      
      def foreground=(value)
        if rendered?
          value = ColorProxy.new(value) if value.is_a?(String)
          @foreground = value
          dom_element.css('color', foreground.to_css) unless foreground.nil?
        else
          enqueue_post_render_method_call('foreground=', value)
        end
      end
      
      def background=(value)
        if rendered?
          value = ColorProxy.new(value) if value.is_a?(String)
          @background = value
          dom_element.css('background-color', background.to_css) unless background.nil?
        else
          enqueue_post_render_method_call('background=', value)
        end
      end
      
      def parent_selector
        @parent&.selector
      end
      
      def parent_dom_element
        if parent
          parent.dom_element
        else
          options[:parent] ||= 'body'
          the_element = Document.find(options[:parent])
          if the_element.length == 0
            options[:parent] = 'body'
            the_element = Document.find('body')
          end
          the_element
        end
      end
      
      def render(parent: nil, custom_parent_dom_element: nil, brand_new: false)
        parent_selector = parent
        options[:parent] = parent_selector if !parent_selector.to_s.empty?
        if !options[:parent].to_s.empty?
          # ensure element is orphaned as it is becoming a top-level root element
          @parent&.post_remove_child(self)
          @parent = nil
        end
        the_parent_dom_element = custom_parent_dom_element || parent_dom_element
        brand_new ||= @dom.nil? || !options[:parent].to_s.empty? || (old_element = dom_element).empty?
        build_dom(layout: !custom_parent_dom_element) # TODO handle custom parent layout by passing parent instead of parent dom element
        if brand_new
          attach(the_parent_dom_element)
        else
          reattach(old_element)
        end
        mark_rendered
        invoke_post_render_method_calls if bulk_render?
        handle_observation_requests
        children.each(&:render) if !bulk_render? && !render_after_create?
        add_contents_for_render_blocks
        notify_on_render_listeners
      end
      alias rerender render
        
      def attach(the_parent_dom_element)
        the_parent_dom_element.append(@dom)
      end
        
      def reattach(old_element)
        old_element.replace_with(@dom)
      end
      
      def mark_rendered
        @rendered = true
        children.each(&:mark_rendered) if bulk_render?
      end
      
      def add_text_content(text, on_empty: false)
        if rendered?
          dom_element.append(text.to_s) if !on_empty || dom_element.text.to_s.empty?
        else
          enqueue_post_render_method_call('add_text_content', text, on_empty:)
        end
      end
      
      def content_on_render_blocks
        @content_on_render_blocks ||= []
      end
      
      def skip_content_on_render_blocks?
        false
      end
      
      def add_content_on_render(&content_block)
        if rendered?
          content_block.call
        else
          content_on_render_blocks << content_block
        end
      end
      
      def build_dom(layout: true)
        # TODO consider passing parent element instead and having table item include a table cell widget only for opal
        @dom = dom # TODO unify how to build dom for most widgets based on element, id, and name (class)
      end
            
      def dom
        # TODO auto-convert known glimmer attributes like parent to data attributes like data-parent
        # TODO check if we need to avoid rendering content block if no content is available
        @dom ||= begin
          content = args.first.is_a?(String) ? args.first : ''
          content += children_dom_content if bulk_render?
          ElementProxy.render_html(keyword, html_options, content)
        end
      end
      
      def children_dom_content
        children.map(&:dom).join
      end
      
      def html_options
        body_class = ([name, element_id] + css_classes.to_a).join(' ')
        html_options = options.dup
        GLIMMER_ATTRIBUTES.each do |attribute|
          next unless html_options.include?(attribute)
          data_normalized_attribute = attribute.split('_').join('-')
          html_options["data-#{data_normalized_attribute}"] = html_options.delete(attribute)
        end
        html_options[:class] ||= ''
        html_options[:class] = "#{html_options[:class]} #{body_class}".strip
        html_options['data-turbo'] = 'false' if parent.nil?
        html_options
      end
      
      def content(bulk_render: false, &block)
        original_bulk_render = options[:bulk_render]
        options[:bulk_render] = bulk_render if rendered?
        return_value = Glimmer::DSL::Engine.add_content(self, Glimmer::DSL::Web::ElementExpression.new, keyword, &block)
        options[:bulk_render] = original_bulk_render if rendered?
        return_value
      end
      
      # Subclasses must override with their own mappings
      def observation_request_to_event_mapping
        {}
      end
      
      def name
        self.class.name.split('::').last.underscore.sub(/_proxy$/, '').gsub('_', '-')
      end
      
      # element ID is used as a css class to identify the element.
      # It is intentionally not set as the actual HTML element ID to let software engineers
      # specify their own IDs if they wanted
      def element_id
        @element_id ||= "element-#{ElementProxy.next_id_number_for(name)}"
      end
      
      def class_name=(value)
        if rendered?
          value = value.is_a?(Array) ? value.join(' ') : value.to_s
          new_class_name = "#{name} #{element_id} #{value}"
          dom_element.prop('className', new_class_name)
        else
          enqueue_post_render_method_call('class_name=', value)
        end
      end
      
      def add_css_class(css_class)
        if rendered?
          dom_element.add_class(css_class)
        else
          enqueue_post_render_method_call('class_name=', value)
        end
      end
      
      def add_css_classes(css_classes_to_add)
        css_classes_to_add.each {|css_class| add_css_class(css_class)}
      end
      
      def remove_css_class(css_class)
        if rendered?
          dom_element.remove_class(css_class)
        else
          enqueue_post_render_method_call('class_name=', value)
        end
      end
      
      def remove_css_classes(css_classes_to_remove)
        css_classes_to_remove.each {|css_class| remove_css_class(css_class)}
      end
      
      def clear_css_classes
        css_classes.each {|css_class| remove_css_class(css_class)}
      end
      
      def dom_element
        if rendered?
          # TODO consider making this pick an element in relation to its parent, allowing unhooked dom elements to be built if needed (unhooked to the visible page dom)
          Document.find(selector)
        else
          # Using a fill-in dom element until self is rendered
          ElementProxy.unrendered_dom_element(keyword)
        end
      end
      
      # TODO consider adding a default #dom method implementation for the common case, automatically relying on #element and other methods to build the dom html
      
      def observation_requests
        @observation_requests ||= {}
      end
      
      def event_listener_proxies
        @event_listener_proxies ||= []
      end
      
      def suspend_event_handling
        @event_handling_suspended = true
      end
      
      def resume_event_handling
        @event_handling_suspended = false
      end
      
      def event_handling_suspended?
        @event_handling_suspended
      end
      
      def listeners
        @listeners ||= {}
      end
      
      def listeners_for(listener_event)
        listeners[listener_event.to_s] ||= []
      end
      
      def can_handle_observation_request?(keyword)
        # TODO sort this out for Opal
        keyword = keyword.to_s
        keyword.start_with?('on')
#         if keyword.start_with?('on_swt_')
#           constant_name = keyword.sub(/^on_swt_/, '')
#           SWTProxy.has_constant?(constant_name)
#         elsif keyword.start_with?('on_')
#           # event = keyword.sub(/^on_/, '')
#           # can_add_listener?(event) || can_handle_drag_observation_request?(keyword) || can_handle_drop_observation_request?(keyword)
#           true # TODO filter by valid listeners only in the future
#         end
      end
      
      def handle_observation_request(keyword, original_event_listener)
        if rendered?
          listener = ListenerProxy.new(
            element: self,
            selector: selector,
            dom_element: dom_element,
            event_attribute: keyword,
            original_event_listener: original_event_listener,
          )
          listener.register
          listeners_for(keyword) << listener
          listener
        else
          enqueue_post_render_method_call('handle_observation_request', keyword, original_event_listener)
        end
      end
      
      def remove_event_listener_proxies
        event_listener_proxies.each do |event_listener_proxy|
          event_listener_proxy.unregister
        end
        event_listener_proxies.clear
      end
      
      def notify_listeners(event)
        listeners_for(event).each do |listener|
          listener.original_event_listener.call(EventProxy.new(listener: listener))
        end
      end
      
      def notify_on_render_listeners
        notify_listeners('on_render')
        children.each(&:notify_on_render_listeners) if bulk_render?
      end
      
      def data_bindings
        @data_bindings ||= {}
      end
      
      def type
        if rendered?
          super
        else
          options[:type] || 'text'
        end
      end
      
      def data_bind(property, model_binding)
        element_binding_read_translator = value_converters_for_input_type(type)&.[](:model_to_view)
        element_binding_parameters = [self, property, element_binding_read_translator]
        element_binding = DataBinding::ElementBinding.new(*element_binding_parameters)
        #TODO make this options observer dependent and all similar observers in element specific data binding handlers
        element_binding.observe(model_binding)
        element_binding.call(model_binding.evaluate_property)
        data_bindings[element_binding] = model_binding
        unless model_binding.binding_options[:read_only]
          # TODO add guards against nil cases for hash below
          listener_keyword = data_binding_listener_for_element_and_property(keyword, property)
          if listener_keyword
            data_binding_read_listener = lambda do |event|
              view_property_value = send(property)
              element_binding_write_translator = value_converters_for_input_type(type)&.[](:view_to_model)
              converted_view_property_value = element_binding_write_translator&.call(view_property_value, model_binding.evaluate_property) || view_property_value
              model_binding.call(converted_view_property_value)
            end
            handle_observation_request(listener_keyword, data_binding_read_listener)
          end
        end
      end
      
      # Data-binds the generation of nested content to a model/property (in binding args)
      # consider providing an option to avoid initial rendering without any changes happening
      def bind_content(*binding_args, &content_block)
        content_binding_work = proc do |*values|
          # TODO in the future, consider optimizing code by diffing content if that makes sense (e.g. using opal-virtual-dom)
          # To do so, we must avoid generating new content with new unique IDs/Classes and only append the new IDs classes after mounting
          # TODO consider optimizing remove performance by doing clear instead and removing listeners separately
          children.dup.each { |child| child.remove }
          content(bulk_render: true, &content_block)
          if bulk_render? && rendered?
            self.inner_html = children_dom_content
            children.each(&:mark_rendered)
            children.each(&:invoke_post_render_method_calls)
            children.each(&:handle_observation_requests)
            children.each(&:add_contents_for_render_blocks)
            children.each(&:notify_on_render_listeners)
          end
        end
        model_binding_observer = Glimmer::DataBinding::ModelBinding.new(*binding_args)
        content_binding_observer = Glimmer::DataBinding::Observer.proc(&content_binding_work)
        content_binding_observer.observe(model_binding_observer)
        content_binding_work.call # TODO inspect if we need to pass args here (from observed attributes) [but it's simpler not to pass anything at first]
      end
      
      def respond_to_missing?(method_name, include_private = false)
        # TODO consider doing more correct checking of availability of properties/methods using native ticks
        property_name = property_name_for(method_name)
        unnormalized_property_name = unnormalized_property_name_for(method_name)
        super(method_name, include_private) ||
          (dom_element && dom_element.length > 0 && Native.call(dom_element, '0').respond_to?(method_name.to_s.camelcase, include_private)) ||
          (dom_element && dom_element.length > 0 && Native.call(dom_element, '0').respond_to?(method_name.to_s, include_private)) ||
          dom_element.respond_to?(method_name, include_private) ||
          (!dom_element.prop(property_name).nil? && !dom_element.prop(property_name).is_a?(Proc)) ||
          (!dom_element.prop(unnormalized_property_name).nil? && !dom_element.prop(unnormalized_property_name).is_a?(Proc)) ||
          method_name.to_s.start_with?('on_')
      end
      
      def method_missing(method_name, *args, &block)
        # TODO consider doing more correct checking of availability of properties/methods using native ticks
        property_name = property_name_for(method_name)
        unnormalized_property_name = unnormalized_property_name_for(method_name)
        if method_name.to_s.start_with?('on_')
          handle_observation_request(method_name, block)
        elsif dom_element.respond_to?(method_name)
          if rendered?
            dom_element.send(method_name, *args, &block)
          else
            enqueue_post_render_method_call(method_name, *args, &block)
          end
        elsif !dom_element.prop(property_name).nil? && !dom_element.prop(property_name).is_a?(Proc)
          if rendered?
            if method_name.end_with?('=')
              dom_element.prop(property_name, *args)
            else
              dom_element.prop(property_name)
            end
          else
            enqueue_post_render_method_call(method_name, *args, &block)
          end
        elsif !dom_element.prop(unnormalized_property_name).nil? && !dom_element.prop(unnormalized_property_name).is_a?(Proc)
          if rendered?
            if method_name.end_with?('=')
              dom_element.prop(unnormalized_property_name, *args)
            else
              dom_element.prop(unnormalized_property_name)
            end
          else
            enqueue_post_render_method_call(method_name, *args, &block)
          end
        elsif dom_element && dom_element.length > 0
          if rendered?
            js_args = block.nil? ? args : (args + [block])
            begin
              Native.call(dom_element, '0').method_missing(method_name.to_s.camelcase, *js_args)
            rescue Exception => e
              begin
                Native.call(dom_element, '0').method_missing(method_name.to_s, *js_args)
              rescue Exception => e
                super(method_name, *args, &block)
              end
            end
          else
            enqueue_post_render_method_call(method_name, *args, &block)
          end
        else
          super(method_name, *args, &block)
        end
      end
      
      def post_render_method_calls
        @post_render_method_calls ||= []
      end
      
      def enqueue_post_render_method_call(method_name, *args, &block)
        post_render_method_calls << [method_name, args, block]
        nil
      end
      
      def invoke_post_render_method_calls
        return unless rendered?
        post_render_method_calls.each do |method_name, args, block|
          send(method_name, *args, &block)
        end
        children.each(&:invoke_post_render_method_calls) if bulk_render?
      end
      
      def handle_observation_requests
        observation_requests&.each do |keyword, event_listener_set|
          event_listener_set.each do |event_listener|
            handle_observation_request(keyword, event_listener)
          end
        end
        children.each(&:handle_observation_requests) if bulk_render?
      end
      
      def add_contents_for_render_blocks
        unless skip_content_on_render_blocks?
          content_on_render_blocks.each do |content_block|
            content(&content_block)
          end
        end
        children.each(&:add_contents_for_render_blocks) if bulk_render?
      end
      
      def property_name_for(method_name)
        attribute_name = method_name.end_with?('=') ? method_name.to_s[0...-1] : method_name.to_s
        PROPERTY_ALIASES[attribute_name] || attribute_name.camelcase
      end
      
      def unnormalized_property_name_for(method_name)
        method_name.end_with?('=') ? method_name.to_s[0...-1] : method_name.to_s
      end
      
      def swt_widget
        # only added for compatibility/adaptibility with Glimmer DSL for SWT
        self
      end
      
      def data_binding_listener_for_element_and_property(element_keyword, property)
        data_binding_property_listener_map_for_element(element_keyword)[property]
      end
      
      def data_binding_property_listener_map_for_element(element_keyword)
        data_binding_element_keyword_to_property_listener_map[element_keyword] || {}
      end
      
      def data_binding_element_keyword_to_property_listener_map
        @data_binding_element_keyword_to_property_listener_map ||= {
          'input' => {
            'value' => 'oninput',
            'checked' => 'oninput',
          },
          'select' => {
            'value' => 'onchange',
          },
          'textarea' => {
            'value' => 'oninput',
          },
        }
      end
      
      def value_converters_for_input_type(input_type)
        input_value_converters[input_type]
      end
      
      def input_value_converters
        @input_value_converters ||= {
          'number' => {
            model_to_view: -> (value, old_value) { value.to_s },
            view_to_model: -> (value, old_value) {
              value.include?('.') ? value.to_f : value.to_i
            },
          },
          'range' => {
            model_to_view: -> (value, old_value) { value.to_s },
            view_to_model: -> (value, old_value) {
              value.include?('.') ? value.to_f : value.to_i
            },
          },
          'datetime-local' => {
            model_to_view: -> (value, old_value) {
              if value.respond_to?(:strftime)
                value.strftime(FORMAT_DATETIME)
              elsif value.is_a?(String) && valid_js_date_string?(value)
                value
              else
                old_value
              end
            },
            view_to_model: -> (value, old_value) {
              if value.to_s.empty?
                nil
              else
                date = Native(`new Date(Date.parse(#{value}))`)
                year = Native.call(date, 'getFullYear')
                month = Native.call(date, 'getMonth') + 1
                day = Native.call(date, 'getDate')
                hour = Native.call(date, 'getHours')
                minute = Native.call(date, 'getMinutes')
                Time.new(year, month, day, hour, minute)
              end
            },
          },
          'date' => {
            model_to_view: -> (value, old_value) {
              if value.respond_to?(:strftime)
                value.strftime(FORMAT_DATE)
              elsif value.is_a?(String) && valid_js_date_string?(value)
                value
              else
                old_value
              end
            },
            view_to_model: -> (value, old_value) {
              if value.to_s.empty?
                nil
              else
                year, month, day = value.split('-')
                if old_value
                  Time.new(year, month, day, old_value.hour, old_value.min)
                else
                  Time.new(year, month, day)
                end
              end
            },
          },
          'time' => {
            model_to_view: -> (value, old_value) {
              if value.respond_to?(:strftime)
                value.strftime(FORMAT_TIME)
              elsif value.is_a?(String) && valid_js_date_string?(value)
                value
              else
                old_value
              end
            },
            view_to_model: -> (value, old_value) {
              if value.to_s.empty?
                nil
              else
                hour, minute = value.split(':')
                if old_value
                  Time.new(old_value.year, old_value.month, old_value.day, hour, minute)
                else
                  now = Time.now
                  Time.new(now.year, now.month, now.day, hour, minute)
                end
              end
            },
          },
        }
      end
      
      private
      
      def valid_js_date_string?(string)
        [REGEX_FORMAT_DATETIME, REGEX_FORMAT_DATE, REGEX_FORMAT_TIME].any? do |format|
          string.match(format)
        end
      end
      
      def css_cursor
        SWT_CURSOR_TO_CSS_CURSOR_MAP[@cursor]
      end
      
    end
  end
end

require 'glimmer/dsl/web/element_expression'