# frozen_string_literal: true

# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later
# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE
# from: https://github.com/reactjs/react-rails/blob/master/lib/generators/react/component_generator.rb
module React
    module Generators
      class ComponentGenerator < ::Rails::Generators::NamedBase
        source_root File.expand_path '../../templates', __FILE__
        desc <<-DESC.strip_heredoc
        Description:
            Scaffold a React component into `components/` of your Webpacker source or asset pipeline.
            The generated component will include a basic render function and a PropTypes
            hash to help with development.
        Available field types:
            Basic prop types do not take any additional arguments. If you do not specify
            a prop type, the generic node will be used. The basic types available are:
            any
            array
            bool
            element
            func
            number
            object
            node
            shape
            string
            Special PropTypes take additional arguments in {}, and must be enclosed in
            single quotes to keep bash from expanding the arguments in {}.
            instanceOf
            takes an optional class name in the form of {className}
            oneOf
            behaves like an enum, and takes an optional list of strings that will
            be allowed in the form of 'name:oneOf{one,two,three}'.
            oneOfType.
            oneOfType takes an optional list of react and custom types in the form of
            'model:oneOfType{string,number,OtherType}'
        Examples:
            rails g react:component person name
            rails g react:component restaurant name:string rating:number owner:instanceOf{Person}
            rails g react:component food 'kind:oneOf{meat,cheese,vegetable}'
            rails g react:component events 'location:oneOfType{string,Restaurant}'
        DESC
  
        argument :attributes,
                 :type => :array,
                 :default => [],
                 :banner => 'field[:type] field[:type] ...'

        class_option :ts,
                     type: :boolean,
                     default: true,
                     desc: 'Output tsx class based component'

        REACT_PROP_TYPES = {
          'node' =>        'PropTypes.node',
          'bool' =>        'PropTypes.bool',
          'boolean' =>     'PropTypes.bool',
          'string' =>      'PropTypes.string',
          'number' =>      'PropTypes.number',
          'object' =>      'PropTypes.object',
          'array' =>       'PropTypes.array',
          'shape' =>       'PropTypes.shape({})',
          'element' =>     'PropTypes.element',
          'func' =>        'PropTypes.func',
          'function' =>    'PropTypes.func',
          'any' =>         'PropTypes.any',
  
          'instanceOf' => ->(type) {
            'PropTypes.instanceOf(%s)' % type.to_s.camelize
          },
  
          'oneOf' => ->(*options) {
            enums = options.map{ |k| "'#{k.to_s}'" }.join(',')
            'PropTypes.oneOf([%s])' % enums
          },
  
          'oneOfType' => ->(*options) {
            types = options.map{ |k| "#{lookup(k.to_s, k.to_s)}" }.join(',')
            'PropTypes.oneOfType([%s])' % types
          }
        }
  
        TYPESCRIPT_TYPES = {
          'node' =>        'React.ReactNode',
          'bool' =>        'boolean',
          'boolean' =>     'boolean',
          'string' =>      'string',
          'number' =>      'number',
          'object' =>      'object',
          'array' =>       'Array<any>',
          'shape' =>       'object',
          'element' =>     'object',
          'func' =>        'object',
          'function' =>    'object',
          'any' =>         'any',
  
          'instanceOf' => ->(type) {
            type.to_s.camelize
          },
  
          'oneOf' => ->(*opts) {
            opts.map{ |k| "'#{k.to_s}'" }.join(" | ")
          },
  
          'oneOfType' => ->(*opts) {
            opts.map{ |k| "#{ts_lookup(k.to_s, k.to_s)}" }.join(" | ")
          }
        }
  
        def create_component_file
          template_extension = if options[:coffee]
            'js.jsx.coffee'
          elsif options[:ts]
            'js.jsx.tsx.erb'
          elsif options[:es6] || webpacker?
            'es6.jsx'
          else
            'js.jsx'
          end
  
          # Prefer webpacker to sprockets:
          if webpacker?
            new_file_name = file_name.camelize
            extension = if options[:coffee]
              'coffee'
            elsif options[:ts]
              'tsx'
            else
              'js'
            end
            target_dir = webpack_configuration.source_path
              .join('components')
              .relative_path_from(::Rails.root)
              .to_s
          else
            new_file_name = file_name
            extension = template_extension
            target_dir = 'app/assets/javascripts/components'
          end
  
          file_path = File.join(target_dir, class_path, "#{new_file_name}.#{extension}")
          template("component.#{template_extension}", file_path)
        end
  
        private
  
        def webpack_configuration
          Webpacker.respond_to?(:config) ? Webpacker.config : Webpacker::Configuration
        end
  
        def component_name
          file_name.camelize
        end
  
        def file_header
          if webpacker?
            return %|import * as React from "react"\n| if options[:ts]
            %|import React from "react"\nimport PropTypes from "prop-types"\n|
          else
            ''
          end
        end
  
        def file_footer
          if webpacker?
            %|export default #{component_name}|
          else
            ''
          end
        end
  
        def webpacker?
          defined?(Webpacker)
        end
  
        def parse_attributes!
          self.attributes = (attributes || []).map do |attr|
            name = ''
            type = ''
            args = ''
            args_regex = /(?<args>{.*})/
  
            name, type = attr.split(':')
  
            if matchdata = args_regex.match(type)
              args = matchdata[:args]
              type = type.gsub(args_regex, '')
            end
  
            if options[:ts]
              { :name => name, :type => ts_lookup(name, type, args), :union => union?(args) }
            else
              { :name => name, :type => lookup(type, args) }
            end
          end
        end
  
        def union?(args = '')
          return args.to_s.gsub(/[{}]/, '').split(',').count > 1
        end
  
        def self.ts_lookup(name, type = 'node', args = '')
          ts_type = TYPESCRIPT_TYPES[type]
          if ts_type.blank?
            if type =~ /^[[:upper:]]/
              ts_type = TYPESCRIPT_TYPES['instanceOf']
            else
              ts_type = TYPESCRIPT_TYPES['node']
            end
          end
  
          args = args.to_s.gsub(/[{}]/, '').split(',')
  
          if ts_type.respond_to? :call
            if args.blank?
              return ts_type.call(type)
            end
  
            ts_type = ts_type.call(*args)
          end
  
          ts_type
        end
  
        def ts_lookup(name, type = 'node', args = '')
          self.class.ts_lookup(name, type, args)
        end
  
         def self.lookup(type = 'node', options = '')
           react_prop_type = REACT_PROP_TYPES[type]
           if react_prop_type.blank?
             if type =~ /^[[:upper:]]/
               react_prop_type = REACT_PROP_TYPES['instanceOf']
             else
               react_prop_type = REACT_PROP_TYPES['node']
             end
           end
  
           options = options.to_s.gsub(/[{}]/, '').split(',')
  
           react_prop_type = react_prop_type.call(*options) if react_prop_type.respond_to? :call
           react_prop_type
         end
  
         def lookup(type = 'node', options = '')
           self.class.lookup(type, options)
         end
      end
    end
  end