Source: components/Model.js

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import collect from '../collect.js';
import axios from '../fetchClient';
import Field from './Field';
import qs from 'qs';
// import PrimordialModel from './PrimordialModel';

// Import traits
import Queryable from './traits/Queryable';
import HasReflection from './traits/HasReflection';
import HasRelations from './traits/HasRelations';
import RenderableAsBasicForm from './traits/RenderableAsBasicForm';
import RenderableAsDiagram from './traits/RenderableAsDiagram';
import RenderableDefault from './traits/RenderableDefault';
import HasKeyProp from './traits/HasKeyProp';
import Eloquent from './traits/Eloquent';
import MorphsDates from './traits/MorphsDates';
import HasAttributes from './traits/HasAttributes';



import Shell from './Shell';

import ReactSync from '../ReactSync';

import { pluralToClassName, classNameToPlural, isModel, studly_case, app_put, app_get, def } from '../helpers';
const pluralize = require('pluralize');

import { filter, flatten, isEmpty, toPairs, pick, kebabCase, snakeCase, difference, intersection } from 'lodash';



/**
  @extends Component
  @mixes Queryable
  @mixes HasReflection
  @mixes HasKeyProp
  @mixes HasRelations
  @mixes RenderableAsBasicForm
  @mixes RenderableAsDiagram
*/
class Model extends Component{

	static get defaultProps(){
		let defaultProps = {
			id: null,
			created_at: null,
			updated_at: null,
		}
		return defaultProps;
	}




/**
  @constructor
  @argument {Object} props An object representing a single model
*/
  constructor(props){

    super(props);

//     if(props === null) debugger

    this.getDates();

    Model.addModel(this.constructor);

	if(!app_get(this.plural)){
		app_put(this.plural, {});
	}

	const res = app_put(`${this.plural}.${this.id}`, this);

	const { singular_url, plural_url } = this.schema.rest_properties;

    this._calculatedProperties = {
		endpoint: `${window.location.protocol}//${window.location.hostname}/${this.constructor.plural}`,
		api_url: `${singular_url}/${this.props.id}`,
		url: `${window.location.protocol}//${window.location.hostname}/${this.constructor.singular}/${this.props.id}`,
    };

    this.state = this.mutableState;

	let relations = filter(toPairs(this.schema), (v) => v[1].type == "relation");
	this.relations = {};

	const joining_ids = [];

	relations.forEach((v_array) => {
		let [ relationName, relationDefinition ] = v_array;
		const { relation_type, definition } = relationDefinition;
		if(relationName in this.props){
// 			console.log('definition', definition);
			joining_ids.push(definition.foreignKey);
			const relationValue = this.props[relationName];
			const class_name = pluralToClassName(relationName);
			const ThisModel = Model.getModel(class_name);
			let relationValueModels = null;
			if(relationValue && ('map' in relationValue)) {
				if(definition.withDefault && isEmpty(relationValue)){
					relationValue.push({});
				}
				relationValueModels = relationValue.map((i, index) => {
					return ThisModel ? new ThisModel({key: `${index}${class_name}${i.id}`, ...i}) : i;
				});
				relationValueModels = collect(relationValueModels);
			}
			else {
				if(definition.withDefault && isEmpty(relationValue)){
					relationValueModels = new ThisModel;
				}
				else{
    				if(isEmpty(relationValue)) relationValueModels = null;
    				else relationValueModels = ThisModel ? new ThisModel(relationValue) : relationValue;
				}
			}
			if(relationValueModels){
				def(this, relationName, () => relationValueModels);
				def(this.relations, relationName, () => relationValueModels);
			}

		}
		else {

			if(typeof this[relation_type] !== 'function'){
				console.error(relation_type, 'this[relation_type]');
			}
			else{
				def(this, relationName, () => this[relation_type](definition));
				def(this.relations, relationName, () => this[relation_type](definition));
			}
			}
		});


		this.setAttributes();

// console.log(joining_ids, 'joining_ids');
		this.constructor.boot(this);
// debugger
	}

	/** */
	hydrate(){
		return new Promise((resolve, reject) => {
			if(this.props.id && Object.keys(this.props).length === 1){
				axios.get(this.api_url).then(response => {
					return resolve(new this.constructor(response.data));
				});
			}
			else{
				return resolve(this);
			}
		});
	}

	/** */
	set_render(render_name){
		const fn = this.props.set_render;

	}

	/** */
	static getPrimaryKey(){
		return  this.getModelProperties().primaryKey;
	}

	/** */
	get primaryKey(){
		return this.constructor.getPrimaryKey();
	}

	/** */
	get id(){
		return '' + this.props[this.primaryKey];
	}

	/** */
	static get editable_props(){
		return [];
	}

	/** */
	get calculatedProperties(){
		return this._calculatedProperties;
	}

    /** */
	static getModelProperties(){
        return ReactSync.getInstance().model_properties[this.name];
	}

	/** */
	get model_properties(){
		return this.constructor.getModelProperties();
	}


	/** */
	static getSchema(){
		return ReactSync.getInstance().schemas[this.name];
	}

	/** */
	get schema(){
		return this.constructor.getSchema();
	}

	/** */
	get url(){
		return this._calculatedProperties.url;
	}

	/** */
	get api_url(){
		return this._calculatedProperties.api_url;
	}

	/** */
	get breadcrumb(){
		let a = document.createElement('a');
		a.href = this._calculatedProperties.url;
		let segments = filter(a.pathname.split('/'));
		let joined = [];
		const reducer = (accumulator, currentValue) => {
			accumulator.push(flatten([...accumulator, currentValue]));
			return accumulator;
		}
		return segments.reduce(reducer, [[]])
			.map(s => `${window.location.protocol}//${window.location.hostname}/${s.join('/')}`);
	}

	/**
	  @todo Implement this as a way to add instantiation items
	*/
	static boot(instance){
		//
	}

	/** */
	filterMutable(objectToFilter){
		// if(!this.constructor.editable_props.length) return objectToFilter;
		return pick(objectToFilter, this.constructor.editable_props);
	}


	/** */
	get mutableState(){
		return this.filterMutable(this.props);
	}


	/** */
	get plural(){
		return this.constructor.plural;
	}

	/** */
	static get plural(){
    	const { plural } = this.getSchema().rest_properties;

		return plural;
	}

	/** */
	get singular(){
		return this.constructor.singular;
	}

	/** */
	static get singular(){
    	const { singular } = this.getSchema().rest_properties;
    	return singular;
	}



	/** */
	get plural_handle(){
		return this.constructor.plural_handle;
	}

	/** */
	static get plural_handle(){
		return snakeCase(this.plural);
	}

	/** */
	get singular_handle(){
		return this.constructor.singular_handle;
	}

	/** */
	static get singular_handle(){
    	return snakeCase(this.singular);
	}

    static get plural_url(){
        return this.getSchema().rest_properties.plural_url;
    }

    static get singular_url(){
        return this.getSchema().rest_properties.singular_url;
    }

    to(){
        return <this.constructor {...this.props} />;
    }


}

new RenderableAsBasicForm(Model);

new RenderableAsDiagram(Model);

new RenderableDefault(Model);

new HasReflection(Model);

new HasRelations(Model);

new HasKeyProp(Model);

new Queryable(Model);

new Eloquent(Model);

new MorphsDates(Model);

new HasAttributes(Model);


(function(){
	/** */
	Model.addModel = function(M){
		Model.models = Model.models || {};
		if(!(M.name in Model.models))
			Model.models[M.name] = M;
	}

	/** */
	Model.getModel = function(M){
		Model.models = Model.models || {};
		const Mname = typeof M === 'string' ? M : M.name;
		return Model.models[Mname];
	}

	/** */
	Model.allModels = function(){
		Model.models = Model.models || {};
		return Model.models;
	}

	/** */
	Model.getRegex = function(){
		const models = collect(Model.allModels());
		if(!models.count()){
			return false;
		}
		const slugs = models.map((v, k) => {
				return [
					k,
					kebabCase(k),
					snakeCase(k),
					pluralize(k),
					v.plural,
					pluralize(kebabCase(k)),
					pluralize(snakeCase(k)),
				];
			})
			.values()
			.collapse()
			.unique()
			.implode('|');

		const regex_string = `/(${slugs})/?([0-9]*?)$`;
		return new RegExp(regex_string, 'i')
	};

	Model.getRegex();

	/** */
	Model.matchUrlPath = function(){
		const regex = Model.getRegex();
		if(!regex){
			return false;
		}
		let path = window.location.pathname;
		if(path.slice(-1) === '/'){
			path = path.slice(0, -1);
		}
		const match_array = window.location.pathname.match(regex);
		if(!match_array){
			return false;
		}
		return Array.from(match_array);
	}

	collect().macro('hydrate', function(){
		return new Promise((resolve, reject) => {
			const needs_hydration = this.filter(i => typeof i !== 'object');
			if(needs_hydration.isEmpty()){
				return resolve(this);
			}
			const _promises = this.items.map(i => {
				if(typeof i !== 'object'){
					return (new this.model({id: i})).hydrate();
				}
				return i;
			});
			Promise.all(_promises)
				.then(x => {
					this.items = x;
					return resolve(this);
				});
		});
	});

	collect().macro('promise', function(){
		return new Promise((resolve, reject) => {
			Promise.all(Object.values(this.items))
				.then(values => {
					this.items = values;
					resolve(this);
				});
		});
	});

	/** */
	Model.extractInstancesFromUrl = function(){
		let matches = Model.matchUrlPath();
		const return_val = {
			models: [],
			instances: {},
		}


		if(!matches){
			return_val.models = collect(return_val.models);
			return_val.instances = collect(return_val.instances);
			return return_val;
		}
		matches = collect(matches).filter().all();
		const model_name = pluralToClassName(matches[1]);
		const M = Model.getModel(model_name);
		if(!M){
			return resolve(return_val);
		}
		const plural = M.plural;
		return_val.models.push(M);
		return_val.instances[plural] = return_val.instances[plural] || [];
		if(matches.length === 3){
			return_val.instances[plural].push(matches[2]);
		}
		return_val.models = collect(return_val.models);
		return_val.instances = collect(return_val.instances);
		return_val.instances = return_val.instances.map((v, k) => {
			const M = Model.getModel(pluralToClassName(k));
			const coll = collect(v);
			coll.model = M;

			return coll.hydrate();
		});

		return return_val;
	}


	/** */
	Model.resolveInstancesFromUrl = function(return_val){
		return new Promise((resolve, reject) => {
			return_val.instances.promise().then((r) => {
				return_val.instances = return_val.instances.keyBy('model.plural').all();
				return resolve(return_val);
			});
		});
	}

/*
	Model.modifyInstance = function(instance, newProps){
		const M = instance.type;
		const resolved_props = {...instance.props, ...newProps};
		return <M {...resolved_props} />;
	}
*/

})();

export default Model;