
/**
 * Return cookie with provided key
 */
function getCookie(key) {
    if(document.cookie && document.cookie !== '') {
        const cookie = document.cookie.split(';')
                               .find(c => c.trim().startsWith(key + '='))
        return cookie ? decodeURIComponent(cookie.split('=')[1]) : null;
    }
    return null;
}

/**
 * CSRF token provided by Django
 */
var csrfToken = null;

/**
 * Get CSRF token
 */
export function getCsrf() {
    if(csrfToken === null)
        csrfToken = getCookie('csrftoken')
    return csrfToken;
}


// TODO: prevent duplicate simple fetch
/**
 * Provide interface used to fetch and manipulate objects.
 */
export default class Model {
    /**
     * Instanciate model with provided data and options.
     * By default `url` is taken from `data.url_`.
     */
    constructor(data={}, {url=null, ...options}={}) {
        this.url = url || data.url_;
        this.options = options;
        this.commit(data);
    }

    get errors() {
        return this.data && this.data.__errors__
    }

    /**
     * Get instance id from its data
     */
    static getId(data) {
        return 'id' in data ? data.id : data.pk;
    }

    /**
     * Return fetch options
     */
    static getOptions(options) {
        return {
            headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json',
                'X-CSRFToken': getCsrf(),
            },
            ...options,
        }
    }

    /**
     * Return model instances for the provided list of model data.
     * @param {Array} items: array of data
     * @param {Object} options: options passed down to all model instances
     */
    static fromList(items, options={}) {
        return items ? items.map(d => new this(d, options)) : []
    }

    /**
     * Fetch item from server
     */
    static fetch(url, {many=false, ...options}={}, args={}) {
        options = this.getOptions(options)
        const request = fetch(url, options).then(response => response.json());
        if(many)
            return request.then(data => {
                if(!(data instanceof Array))
                    data = data.results
                return this.fromList(data, args)
            })
        else
            return request.then(data => new this(data, {url: url, ...args}));
    }

    /**
     * Fetch data from server.
     */
    fetch(options) {
        options = this.constructor.getOptions(options)
        return fetch(this.url, options)
            .then(response => response.json())
            .then(data => this.commit(data));
    }

    /**
     * Call API action on object.
     */
    action(path, options, commit=false) {
        options = this.constructor.getOptions(options)
        const promise = fetch(this.url + path, options);
        return commit ? promise.then(data => data.json())
                               .then(data => { this.commit(data); this.data })
                      : promise;
    }

    /**
     * Update instance's data with provided data. Return None
     */
    commit(data) {
        this.data = data;
        this.id = this.constructor.getId(this.data);
    }

    /**
     * Update model data, without reset previous value
     */
    update(data) {
        this.data = {...this.data, ...data}
        this.id = this.constructor.getId(this.data)
    }

    /**
     * Save instance into localStorage.
     */
    store(key) {
        window.localStorage.setItem(key, JSON.stringify(this.data));
    }

    /**
     * Load model instance from localStorage.
     */
    static storeLoad(key) {
        let item = window.localStorage.getItem(key);
        return item === null ? item : new this(JSON.parse(item));
    }

    /**
     * Return true if model instance has no data
     */
    get isEmpty() {
        return !this.data || Object.keys(this.data).findIndex(k => !!this.data[k] && this.data[k] !== 0) == -1
    }

    /**
     * Return error for a specific attribute name if any
     */
    error(attr=null) {
        return attr === null ? this.errors : this.errors && this.errors[attr]
    }
}




/**
 * List of models
 */
export class Set {
    constructor(model, {items=[],url=null,args={},unique=null,max=null,storeKey=null}={}) {
        this.items = [];
        this.model = model;
        this.url = url;
        this.unique = unique;
        this.max = max;
        this.storeKey = storeKey;

        for(var item of items)
            this.push(item, {args: args, save: false});
    }

    get length() { return this.items.length }

    /**
     * Fetch multiple items from server
     */
    static fetch(model, url, options=null, args=null) {
        options = model.getOptions(options)
        return fetch(url, options)
            .then(response => response.json())
            .then(data => (data instanceof Array ? data : data.results)
                              .map(d => new model(d, {url: url, ...args})))
    }

    /**
     * Load list from localStorage
     */
    static storeLoad(model, key, args={}) {
        let items = window.localStorage.getItem(key);
        return new this(model, {...args, storeKey: key, items: items ? JSON.parse(items) : []});
    }

    /**
     * Store list into localStorage
     */
    store() {
        this.storeKey && window.localStorage.setItem(this.storeKey, JSON.stringify(
            this.items.map(i => i.data)));
    }

    /**
     * Save item
     */
    save() {
        this.storeKey && this.store();
    }

    /**
     * Get item at index
     */
    get(index) { return this.items[index] }

    /**
     * Find an item by id or using a predicate function
     */
    find(pred) {
        return pred instanceof Function ? this.items.find(pred)
                                        : this.items.find(x => x.id == pred.id);
    }

    /**
     * Find item index by id or using a predicate function
     */
    findIndex(pred) {
        return pred instanceof Function ? this.items.findIndex(pred)
                                        : this.items.findIndex(x => x.id == pred.id);
    }

    /**
     * Add item to set, return index.
     */
    push(item, {args={},save=true}={}) {
        item = item instanceof this.model ? item : new this.model(item, args);
        if(this.unique) {
            let index = this.findIndex(item);
            if(index > -1)
                return index;
        }
        if(this.max && this.items.length >= this.max)
            this.items.splice(0,this.items.length-this.max)

        this.items.push(item);
        save && this.save();
        return this.items.length-1;
    }

    /**
     * Remove item from set by index
     */
    remove(index, {save=true}={}) {
        this.items.splice(index,1);
        save && this.save();
    }

    /**
     * Clear items, assign new ones
     */
    reset(items=[]) {
        // TODO: check reactivity
        this.items = []
        for(var item of items)
            this.push(item)
    }

    move(from, to) {
        if(from >= this.length || to > this.length)
            throw "source or target index is not in range"

        const value = this.items[from]
        this.items.splice(from, 1)
        this.items.splice(to, 0, value)
    }
}

Set[Symbol.iterator] = function () {
    return this.items[Symbol.iterator]();
}
