import { Config } from "../../../../../ontologies/SparnaturalConfig";
import Datasources from "../../../../../ontologies/SparnaturalConfigDatasources";
import { Catalog } from "../../../../../settings/Catalog";
import HTMLComponent from "../../../../HtmlComponent";
import { SelectedVal } from "../../../../SelectedVal";
import { AbstractWidget } from "../../../../widgets/AbstractWidget";
import { AutocompleteConfiguration, AutoCompleteWidget } from "../../../../widgets/AutoCompleteWidget";
import { BooleanWidget } from "../../../../widgets/BooleanWidget";
import { ListDataProviderIfc, NoOpListDataProvider, SparqlListDataProvider, SortListDataProvider, AutocompleteDataProviderIfc, NoOpAutocompleteProvider, SparqlAutocompleDataProvider, TreeDataProviderIfc, NoOpTreeDataProvider, SparqlTreeDataProvider, SortTreeDataProvider } from "../../../../widgets/data/DataProviders";
import { ListSparqlTemplateQueryBuilder, AutocompleteSparqlTemplateQueryBuilder, TreeSparqlTemplateQueryBuilder } from "../../../../widgets/data/SparqlBuilders";
import { SparqlHandlerFactory, SparqlHandlerIfc } from "../../../../widgets/data/SparqlHandler";
import { ListConfiguration, ListWidget } from "../../../../widgets/ListWidget";
import MapWidget, { MapConfiguration } from "../../../../widgets/MapWidget";
import { NoWidget } from "../../../../widgets/NoWidget";
import { NumberConfiguration, NumberWidget } from "../../../../widgets/NumberWidget";
import { SearchConfiguration, SearchRegexWidget } from "../../../../widgets/SearchRegexWidget";
import { TimeDatePickerWidget } from "../../../../widgets/timedatepickerwidget/TimeDatePickerWidget";
import { TreeConfiguration, TreeWidget } from "../../../../widgets/treewidget/TreeWidget";

/**
 * Inversion of coupling : we don't want to depend on ISettings as this class is meant to be reused
 * elsewhere than in Sparnatural, hence we define our own interface of what we depend on.
 */
export class WidgetFactorySettings {

    language: string;
    defaultLanguage: string;
    typePredicate: string;
    maxOr?: number;
    
    sparqlPrefixes?: { [key: string]: string };
    endpoints?: string[];
    catalog?: string;
    localCacheDataTtl?: number;
    
    customization? : {
      autocomplete?: Partial<AutocompleteConfiguration>,
      list?: Partial<ListConfiguration>,   
      tree?: Partial<TreeConfiguration>,
      number?: Partial<NumberConfiguration>,
      map?: Partial<MapConfiguration>,
      headers?: Map<string,string>,
      sparqlHandler?: SparqlHandlerIfc
    }
}



export class WidgetFactory {

    parentComponent:HTMLComponent;
    specProvider: any;
    settings: WidgetFactorySettings;
    catalog?:Catalog;

    private sparqlFetcherFactory:SparqlHandlerFactory;
    private sparqlPostProcessor:{ semanticPostProcess: (sparql:string)=>string };

    constructor(
        parentComponent:HTMLComponent,
        specProvider: any,
        settings: WidgetFactorySettings,
        catalog:Catalog,
    ) {
        this.parentComponent = parentComponent;
        this.specProvider = specProvider;
        this.settings = settings;
        this.catalog = catalog;

        // how to fetch a SPARQL query
        this.sparqlFetcherFactory = new SparqlHandlerFactory(            
            this.settings.language,
            this.settings.localCacheDataTtl,
            this.settings.customization.headers,
            this.settings.customization.sparqlHandler,
            this.catalog
        );

        // how to post-process the generated SPARQL after it is constructed and before it is send
        this.sparqlPostProcessor = { semanticPostProcess: (sparql: any) => {
            // also add prefixes
            for (let key in this.settings.sparqlPrefixes) {
              sparql = sparql.replace(
                "SELECT ",
                "PREFIX " +
                  key +
                  ": <" +
                  this.settings.sparqlPrefixes[key] +
                  "> \nSELECT "
              );
            }
            return this.specProvider.expandSparql(sparql, this.settings.sparqlPrefixes);
          }
        }
    }


    /**
     * Create a widget.
     * 
     * @param widgetType the widget type(list, autocomplete, etc.) that is returned from the method "getPropertyType" of the configuration
     * @param startClassVal the type + variable name of the subject
     * @param objectPropVal the type + variable name of the property
     * @param endClassVal the type + variable name of the object
     * @returns the widget component to be displayed
     */
    buildWidget(
        widgetType: string,
        startClassVal: SelectedVal,
        objectPropVal: SelectedVal,
        endClassVal: SelectedVal,
    ): AbstractWidget {
        switch (widgetType) {
          case Config.LITERAL_LIST_PROPERTY:
          case Config.LIST_PROPERTY:
            // to be passed in anonymous functions
            var theSpecProvider = this.specProvider;
    
            // determine custom datasource
            var datasource = this.specProvider.getProperty(objectPropVal.type).getDatasource();
    
            if (datasource == null) {
              // datasource still null
              // if a default endpoint was provided, provide default datasource
              if (this.settings.endpoints || this.settings.catalog) {
    
                // if there is a default label property on the end class, use it to populate the dropdown
                if(this.specProvider.getEntity(endClassVal.type).getDefaultLabelProperty()) {
                  datasource = {
                    queryTemplate: Datasources.QUERY_STRINGS_BY_QUERY_TEMPLATE.get(
                      Datasources.QUERY_LIST_LABEL_ALPHA
                    ),
                    labelProperty: this.specProvider.getEntity(endClassVal.type).getDefaultLabelProperty(),
                  }
                } else {
                  // that datasource can work indifferently with URIs or Literals
                  datasource = Datasources.DATASOURCES_CONFIG.get(
                    // better use alphabetical ordering first since URIs will be segregated in the "h" letter and not mixed
                    // Datasources.LIST_URI_OR_LITERAL_ALPHA_WITH_COUNT
                    Datasources.LIST_URI_OR_LITERAL_ALPHA
                  );
                }
    
              }
            }
    
            let listDataProvider:ListDataProviderIfc = new NoOpListDataProvider();
            if (datasource != null) {
              // if we have a datasource, possibly the default one, provide a config based
              // on a SparqlTemplate, otherwise use the handler provided
              listDataProvider = new SparqlListDataProvider(
    
                // endpoint URL
                this.sparqlFetcherFactory.buildSparqlHandler(
                    datasource.sparqlEndpointUrl != null
                    ? [datasource.sparqlEndpointUrl]
                    : this.settings.endpoints
                ), 
    
                new ListSparqlTemplateQueryBuilder(
                  // sparql query (with labelPath interpreted)
                  this.#getFinalQueryString(datasource),
    
                  // sparqlPostProcessor
                  this.sparqlPostProcessor
                )
              );
            }
    
            // if we need to sort things, add an explicit wrapper around the data provider
            if(!(datasource.noSort == true)) {
              listDataProvider = new SortListDataProvider(listDataProvider);
            }
    
            // create the configuration object : use the default configuration, then the generated data provider, then overwrite with 
            // what is set in the provided configuration object for the corresponding section
            let listConfig:ListConfiguration = {
              ...ListWidget.defaultConfiguration,
              ...{
                dataProvider: listDataProvider,
                values:this.specProvider.getProperty(objectPropVal.type).getValues(),
              },          
              ...this.settings.customization?.list
            };
    
            // init data provider
            listConfig.dataProvider.init(
              this.settings.language,
              this.settings.defaultLanguage,
              this.settings.typePredicate
            );
    
            return new ListWidget(
              this.parentComponent,
              listConfig,
              startClassVal,
              objectPropVal,
              endClassVal
            );
    
          case Config.AUTOCOMPLETE_PROPERTY:
    
            // to be passed in anonymous functions
            var theSpecProvider = this.specProvider;
    
            // determine custom datasource
            var datasource = this.specProvider.getProperty(objectPropVal.type).getDatasource();
    
            if (datasource == null) {
              // datasource still null
              // if a default endpoint was provided, provide default datasource
              if (this.settings.endpoints) {
                if(this.specProvider.getEntity(endClassVal.type).isLiteralEntity()) {
                  datasource = Datasources.DATASOURCES_CONFIG.get(
                    Datasources.SEARCH_LITERAL_CONTAINS
                  );
                } else {
    
                  // if there is a default label property on the end class, use it to search in the autocomplete
                  if(this.specProvider.getEntity(endClassVal.type).getDefaultLabelProperty()) {
                    datasource = {
                      queryTemplate: Datasources.QUERY_STRINGS_BY_QUERY_TEMPLATE.get(
                        Datasources.QUERY_SEARCH_LABEL_CONTAINS
                      ),
                      labelProperty: this.specProvider.getEntity(endClassVal.type).getDefaultLabelProperty(),
                    }
                  } else {
                    // otherwise just search on the URI 
                    datasource = Datasources.DATASOURCES_CONFIG.get(
                      Datasources.SEARCH_URI_CONTAINS
                    );
                  }              
                }
              }
            }
    
            let autocompleteDataProvider:AutocompleteDataProviderIfc = new NoOpAutocompleteProvider();
            if (datasource != null) {
              // build a SPARQL data provider function using the SPARQL query of the datasource
              autocompleteDataProvider = new SparqlAutocompleDataProvider(
    
                // endpoint URL
                this.sparqlFetcherFactory.buildSparqlHandler(
                    datasource.sparqlEndpointUrl != null
                    ? [datasource.sparqlEndpointUrl]
                    : this.settings.endpoints
                ), 
    
                new AutocompleteSparqlTemplateQueryBuilder(
                  // sparql query (with labelPath interpreted)
                  this.#getFinalQueryString(datasource),
    
                  // sparqlPostProcessor
                  this.sparqlPostProcessor
                )
              );
            }
    
            // create the configuration object : use the default data provider, then the default configuration, then overwrite with is set in 
            // the provided configuration object for the corresponding section
            let autocompleteConfig:AutocompleteConfiguration = {
              ...AutoCompleteWidget.defaultConfiguration,
              ...{dataProvider: autocompleteDataProvider},
              ...this.settings.customization?.autocomplete
            };
    
            // init data provider
            autocompleteConfig.dataProvider.init(
              this.settings.language,
              this.settings.defaultLanguage,
              this.settings.typePredicate
            );
    
            return new AutoCompleteWidget(
              this.parentComponent,
              autocompleteConfig,
              startClassVal,
              objectPropVal,
              endClassVal
            );
    
            break;
          case Config.VIRTUOSO_SEARCH_PROPERTY:
          case Config.GRAPHDB_SEARCH_PROPERTY:
          case Config.STRING_EQUALS_PROPERTY:
          case Config.SEARCH_PROPERTY:
    
            let configuration:SearchConfiguration = {
              widgetType: widgetType
            }
    
            return new SearchRegexWidget(
              configuration,
              this.parentComponent,
              startClassVal,
              objectPropVal,
              endClassVal
            );
            break;
          case Config.TIME_PROPERTY_YEAR:
            return new TimeDatePickerWidget(
              this.parentComponent,
              "year",
              startClassVal,
              objectPropVal,
              endClassVal,
              this.specProvider
            );
            break;
          case Config.TIME_PROPERTY_DATE:
            return new TimeDatePickerWidget(
              this.parentComponent,
              "day",
              startClassVal,
              objectPropVal,
              endClassVal,
              this.specProvider
            );
            break;
          case Config.TIME_PROPERTY_PERIOD:
            console.warn(Config.TIME_PROPERTY_PERIOD+" is not implement yet");
            break;
          case Config.NON_SELECTABLE_PROPERTY:
            return new NoWidget(this.parentComponent);
            break;
          case Config.BOOLEAN_PROPERTY:
            return new BooleanWidget(
              this.parentComponent,
              startClassVal,
              objectPropVal,
              endClassVal
            );
            break;
          case Config.TREE_PROPERTY:
            var theSpecProvider = this.specProvider;
    
            // determine custom roots datasource
            var treeRootsDatasource =
              this.specProvider.getProperty(objectPropVal.type).getTreeRootsDatasource();
    
            if (treeRootsDatasource == null) {
              // datasource still null
              // if a default endpoint was provided, provide default datasource
              if (this.settings.endpoints) {
                treeRootsDatasource = Datasources.DATASOURCES_CONFIG.get(
                  Datasources.TREE_ROOT_SKOSTOPCONCEPT
                );
              }
            }
    
            // determine custom children datasource
            var treeChildrenDatasource =
              this.specProvider.getProperty(objectPropVal.type).getTreeChildrenDatasource();
    
            if (treeChildrenDatasource == null) {
              // datasource still null
              // if a default endpoint was provided, provide default datasource
              if (this.settings.endpoints) {
                treeChildrenDatasource = Datasources.DATASOURCES_CONFIG.get(
                  Datasources.TREE_CHILDREN_SKOSNARROWER
                );
              }
            }
    
            let treeDataProvider:TreeDataProviderIfc = new NoOpTreeDataProvider();
    
            if (treeRootsDatasource != null && treeChildrenDatasource != null) {
              // if we have a datasource, possibly the default one, provide a config based
              // on a SparqlTemplate, otherwise use the handler provided
    
              treeDataProvider = new SparqlTreeDataProvider(
    
                // endpoint URL
                // we read it on the roots datasource
                this.sparqlFetcherFactory.buildSparqlHandler(
                    treeRootsDatasource.sparqlEndpointUrl != null
                    ? [treeRootsDatasource.sparqlEndpointUrl]
                    : this.settings.endpoints
                ),
    
                new TreeSparqlTemplateQueryBuilder(
                  // sparql query (with labelPath interpreted)
                  this.#getFinalQueryString(treeRootsDatasource),
                  this.#getFinalQueryString(treeChildrenDatasource),
    
                  // sparqlPostProcessor
                  this.sparqlPostProcessor
                )
    
              );
            }
    
            // create the configuration object : use the default data provider, then the default configuration, then overwrite with is set in 
            // the provided configuration object for the corresponding section
            let treeConfig:TreeConfiguration = {
              ...TreeWidget.defaultConfiguration,
              ...{
                  dataProvider: treeDataProvider,
                  maxSelectedItems: this.settings.maxOr
              },
              ...this.settings.customization?.tree
            };
    
            // wrap inside a sort data provider if needed
            if(!(treeChildrenDatasource.noSort == true)) {
              treeConfig.dataProvider = new SortTreeDataProvider(treeConfig.dataProvider);
            }
    
            // init data provider
            treeConfig.dataProvider.init(
              this.settings.language,
              this.settings.defaultLanguage,
              this.settings.typePredicate
            );
          
            return new TreeWidget(
              this.parentComponent,
              treeConfig,
              startClassVal,
              objectPropVal,
              endClassVal
            );
    
          case Config.MAP_PROPERTY:
            let mapConfig:MapConfiguration = {
              ...MapWidget.defaultConfiguration,
              ...this.settings.customization?.map
            };
    
            return new MapWidget(
              mapConfig,
              this.parentComponent,
              startClassVal,
              objectPropVal,
              endClassVal
            ).render();
          
          case Config.NUMBER_PROPERTY:
    
            // TODO : determine min and max based on datatypes
            let thisNumberConfig:NumberConfiguration = {
              min: this.specProvider.getProperty(objectPropVal.type).getMinValue(),
              max: this.specProvider.getProperty(objectPropVal.type).getMaxValue(),
            }
    
            let numberConfig:NumberConfiguration = {
              ...NumberWidget.defaultConfiguration,
              ...thisNumberConfig,
              ...this.settings.customization?.number
            };
    
            return new NumberWidget(
              this.parentComponent,
              numberConfig,
              startClassVal,
              objectPropVal,
              endClassVal
            ).render();
    
          default:
            throw new Error(`WidgetType for ${widgetType} not recognized`);
        }
      }

    /**
     * Builds the final query string from a query source, by injecting
     * labelPath/property and childrenPath/property
     **/
    #getFinalQueryString(datasource: any) {
        if (datasource.queryString != null) {
          return datasource.queryString;
        } else {
          var sparql = datasource.queryTemplate;

          if (datasource.labelPath != null || datasource.labelProperty) {
              var theLabelPath = datasource.labelPath
              ? datasource.labelPath
              : "<" + datasource.labelProperty + ">";
              var reLabelPath = new RegExp("\\$labelPath", "g");
              sparql = sparql.replace(reLabelPath, theLabelPath);
          }

          if (datasource.childrenPath != null || datasource.childrenProperty) {
              var theChildrenPath = datasource.childrenPath
              ? datasource.childrenPath
              : "<" + datasource.childrenProperty + ">";
              var reChildrenPath = new RegExp("\\$childrenPath", "g");
              sparql = sparql.replace(reChildrenPath, theChildrenPath);
          }

          return sparql;
        }
    }

}