# 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-dsl-web'

unless Object.const_defined?(:Address)
  Address = Struct.new(:full_name, :street, :street2, :city, :state, :zip_code, :billing_and_shipping, keyword_init: true) do
    STATES = {
      "AK"=>"Alaska", "AL"=>"Alabama", "AR"=>"Arkansas", "AS"=>"American Samoa", "AZ"=>"Arizona",
      "CA"=>"California", "CO"=>"Colorado", "CT"=>"Connecticut", "DC"=>"District of Columbia", "DE"=>"Delaware",
      "FL"=>"Florida", "GA"=>"Georgia", "GU"=>"Guam", "HI"=>"Hawaii", "IA"=>"Iowa", "ID"=>"Idaho", "IL"=>"Illinois",
      "IN"=>"Indiana", "KS"=>"Kansas", "KY"=>"Kentucky", "LA"=>"Louisiana", "MA"=>"Massachusetts", "MD"=>"Maryland",
      "ME"=>"Maine", "MI"=>"Michigan", "MN"=>"Minnesota", "MO"=>"Missouri", "MS"=>"Mississippi", "MT"=>"Montana",
      "NC"=>"North Carolina", "ND"=>"North Dakota", "NE"=>"Nebraska", "NH"=>"New Hampshire", "NJ"=>"New Jersey",
      "NM"=>"New Mexico", "NV"=>"Nevada", "NY"=>"New York", "OH"=>"Ohio", "OK"=>"Oklahoma", "OR"=>"Oregon",
      "PA"=>"Pennsylvania", "PR"=>"Puerto Rico", "RI"=>"Rhode Island", "SC"=>"South Carolina", "SD"=>"South Dakota",
      "TN"=>"Tennessee", "TX"=>"Texas", "UT"=>"Utah", "VA"=>"Virginia", "VI"=>"Virgin Islands", "VT"=>"Vermont",
      "WA"=>"Washington", "WI"=>"Wisconsin", "WV"=>"West Virginia", "WY"=>"Wyoming"
    }
    
    def state_code
      STATES.invert[state]
    end
    
    def state_code=(value)
      self.state = STATES[value]
    end
  
    def summary
      string_attributes = to_h.except(:billing_and_shipping)
      summary = string_attributes.values.map(&:to_s).reject(&:empty?).join(', ')
      summary += " (Billing & Shipping)" if billing_and_shipping
      summary
    end
  end
end

unless Object.const_defined?(:AddressForm)
  # AddressForm Glimmer Web Component (View component)
  #
  # Including Glimmer::Web::Component makes this class a View component and automatically
  # generates a new Glimmer HTML DSL keyword that matches the lowercase underscored version
  # of the name of the class. AddressForm generates address_form keyword, which can be used
  # elsewhere in Glimmer HTML DSL code as done inside HelloComponentListeners below.
  class AddressForm
    include Glimmer::Web::Component
    
    option :address
    
    markup {
      div {
        div(style: {display: :grid, grid_auto_columns: '80px 260px'}) { |address_div|
          label('Full Name: ', for: 'full-name-field')
          input(id: 'full-name-field') {
            value <=> [address, :full_name]
          }
          
          label('Street: ', for: 'street-field')
          input(id: 'street-field') {
            value <=> [address, :street]
          }
          
          label('Street 2: ', for: 'street2-field')
          textarea(id: 'street2-field') {
            value <=> [address, :street2]
          }
          
          label('City: ', for: 'city-field')
          input(id: 'city-field') {
            value <=> [address, :city]
          }
          
          label('State: ', for: 'state-field')
          select(id: 'state-field') {
            Address::STATES.each do |state_code, state|
              option(value: state_code) { state }
            end
  
            value <=> [address, :state_code]
          }
          
          label('Zip Code: ', for: 'zip-code-field')
          input(id: 'zip-code-field', type: 'number', min: '0', max: '99999') {
            value <=> [address, :zip_code,
                        on_write: :to_s,
                      ]
          }
          
          style {
            r("#{address_div.selector} *") {
              margin '5px'
            }
            r("#{address_div.selector} input, #{address_div.selector} select") {
              grid_column '2'
            }
          }
        }
        
        div(style: {margin: 5}) {
          inner_text <= [address, :summary,
                          computed_by: address.members + ['state_code'],
                        ]
        }
      }
    }
  end
end

unless Object.const_defined?(:AccordionSection)
  class AccordionSection
    class Presenter
      attr_accessor :collapsed, :instant_transition
      
      def toggle_collapsed(instant: false)
        self.instant_transition = instant
        self.collapsed = !collapsed
      end
      
      def expand(instant: false)
        self.instant_transition = instant
        self.collapsed = false
      end
      
      def collapse(instant: false)
        self.instant_transition = instant
        self.collapsed = true
      end
    end
    
    include Glimmer::Web::Component
    
    events :expanded, :collapsed
    
    option :title
    
    attr_reader :presenter
    
    before_render do
      @presenter = Presenter.new
    end
    
    markup {
      section {
        # Unidirectionally data-bind the class inclusion of 'collapsed' to the @presenter.collapsed boolean attribute,
        # meaning if @presenter.collapsed changes to true, the CSS class 'collapsed' is included on the element,
        # and if it changes to false, the CSS class 'collapsed' is removed from the element.
        class_name(:collapsed) <= [@presenter, :collapsed]
        class_name(:instant_transition) <= [@presenter, :instant_transition]
      
        header(title, class: 'accordion-section-title') {
          onclick do |event|
            @presenter.toggle_collapsed
            if @presenter.collapsed
              notify_listeners(:collapsed)
            else
              notify_listeners(:expanded)
            end
          end
        }
        
        div(slot: :section_content, class: 'accordion-section-content')
      }
    }
    
    style {
      r('.accordion-section-title') {
        font_size 2.em
        font_weight :bold
        cursor :pointer
        padding_left 20
        position :relative
        margin_block_start 0.33.em
        margin_block_end 0.33.em
      }
      
      r('.accordion-section-title::before') {
        content '"▼"'
        position :absolute
        font_size 0.5.em
        top 10
        left 0
      }
      
      r('.accordion-section-content') {
        height 246
        overflow :hidden
        transition 'height 0.5s linear'
      }
      
      r("#{component_element_selector}.instant_transition .accordion-section-content") {
        transition 'initial'
      }
      
      r("#{component_element_selector}.collapsed .accordion-section-title::before") {
        content '"►"'
      }
      
      r("#{component_element_selector}.collapsed .accordion-section-content") {
        height 0
      }
    }
  end
end

unless Object.const_defined?(:Accordion)
  class Accordion
    include Glimmer::Web::Component
    
    events :accordion_section_expanded, :accordion_section_collapsed
    
    markup {
      # given that no slots are specified, nesting content under the accordion component
      # in consumer code adds content directly inside the markup root div.
      div { |accordion|
        # on render, all accordion sections would have been added by consumers already, so we can
        # attach listeners to all of them by re-opening their content with `.content { ... }` block
        on_render do
          accordion_section_elements = accordion.children
          accordion_sections = accordion_section_elements.map(&:component)
          accordion_sections.each_with_index do |accordion_section, index|
            accordion_section_number = index + 1
  
            # ensure only the first section is expanded
            accordion_section.presenter.collapse(instant: true) if accordion_section_number != 1
  
            accordion_section.content {
              on_expanded do
                other_accordion_sections = accordion_sections.reject {|other_accordion_section| other_accordion_section == accordion_section }
                other_accordion_sections.each { |other_accordion_section| other_accordion_section.presenter.collapse }
                notify_listeners(:accordion_section_expanded, accordion_section_number)
              end
  
              on_collapsed do
                notify_listeners(:accordion_section_collapsed, accordion_section_number)
              end
            }
          end
        end
      }
    }
  end
end

unless Object.const_defined?(:HelloComponentListeners)
  # HelloComponentListeners Glimmer Web Component (View component)
  #
  # This View component represents the main page being rendered,
  # as done by its `render` class method below
  #
  # Note: check out HelloComponentListenersDefaultSlot for a simpler version that leverages the default slot feature
  class HelloComponentListeners
    class Presenter
      attr_accessor :status_message
      
      def initialize
        @status_message = "Accordion section 1 is expanded!"
      end
    end
    
    include Glimmer::Web::Component
    
    before_render do
      @presenter = Presenter.new
      @shipping_address = Address.new(
        full_name: 'Johnny Doe',
        street: '3922 Park Ave',
        street2: 'PO BOX 8382',
        city: 'San Diego',
        state: 'California',
        zip_code: '91913',
      )
      @billing_address = Address.new(
        full_name: 'John C Doe',
        street: '123 Main St',
        street2: 'Apartment 3C',
        city: 'San Diego',
        state: 'California',
        zip_code: '91911',
      )
      @emergency_address = Address.new(
        full_name: 'Mary Doe',
        street: '2038 Ipswitch St',
        street2: 'Suite 300',
        city: 'San Diego',
        state: 'California',
        zip_code: '91912',
      )
    end
    
    markup {
      div {
        h1(style: {font_style: :italic}) {
          inner_html <= [@presenter, :status_message]
        }
          
        accordion { # any content nested under component directly is added under its markup root div element
          accordion_section(title: 'Shipping Address') {
            section_content { # contribute elements to section_content slot declared in AccordionSection component
              address_form(address: @shipping_address)
            }
          }
          
          accordion_section(title: 'Billing Address') {
            section_content {
              address_form(address: @billing_address)
            }
          }
          
          accordion_section(title: 'Emergency Address') {
            section_content {
              address_form(address: @emergency_address)
            }
          }
          
          # on_accordion_section_expanded listener matches event :accordion_section_expanded declared in Accordion component
          on_accordion_section_expanded { |accordion_section_number|
            @presenter.status_message = "Accordion section #{accordion_section_number} is expanded!"
          }
          
          on_accordion_section_collapsed { |accordion_section_number|
            @presenter.status_message = "Accordion section #{accordion_section_number} is collapsed!"
          }
        }
      }
    }
  end
end

Document.ready? do
  # renders a top-level (root) HelloComponentListeners component
  # Note: check out hello_component_listeners_default_slot.rb for a simpler version that leverages the default slot feature
  HelloComponentListeners.render
end