const R = require("ramda")
export * from "./getXrmP"
import { XRM } from "./xrm"
import { DEBUG} from "BuildSettings"

/**
 * Get a URL parameter from `search` or `document.loction.search` by
 * default. This is useful for obtaining the "data" parameter from the URL
 * that is passed in from a form if you set the WebResource url properties.
 * Sometimes the API does not seem to work, but this seems to always work.
 */
export function getURLParameter(name: string, search: string = document.location.search): string | null {
    search = search || ""
    const r: Array<string> | null = new RegExp("[?|&]" + name + "=" + "([^&;]+?)(&|#|;|$)").exec(search)
    const r2: Array<string> = ["", ""]
    return decodeURIComponent((r || r2)[1].replace(/\+/g, "%20")) || null;
}

/** Generate a unique id with an optional prefix. */
export function generateId(prefix: string = "") {
    return `${prefix}-${uuidv4()}`
}

const dec2hex: string[] = [];
for (let i = 0; i <= 15; i++) {
    dec2hex[i] = i.toString(16);
}

const UUID = () => {
    let uuid = ""
    for (let i = 1; i <= 36; i++) {
        if (i === 9 || i === 14 || i === 19 || i === 24) {
            uuid += "-"
        } else if (i === 15) {
            uuid += 4
        } else if (i === 20) {
            uuid += dec2hex[(Math.random() * 4 | 0 + 8)]
        } else {
            uuid += dec2hex[(Math.random() * 15 | 0)]
        }
    }
    return uuid;
}

/** Probably need something multi-browser friendly here. */
//export function uuidv4(): string {
//  return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
//    (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
//  )
//}
export function uuidv4(): string {
    return UUID()
}

/** Internal to this module. ... */
function cleanId(id: string): string {
    if (typeof id === "undefined" || id === null) throw Error(`Unable to clean nil id ${id}`)
    return id.toString().replace(/[{}]/g, "").toLowerCase()
}

/** Uses internal API. */
export function isUci(xrm: XRM): boolean {
    if (xrm.Internal && xrm.Internal.isUci)
        return xrm.Internal.isUci()
    return false
}

let _isElectron: boolean = false

const userAgent = navigator.userAgent.toLowerCase();
if (userAgent.indexOf(" electron/") > -1) {
    _isElectron = true
}

/** Return true if we are running inside electron. */
export function isElectron(): boolean {
    return _isElectron
}

/**
 * Checks the form context to see if there is an entity id. If not,
 * it's probably a new form.
 */
export function hasEntityId(xrm: XRM | null): boolean {
    if (!xrm) return false
    const id = entityIdOrNull(xrm)
    if (id) return true
    return false
}

/** Return the entity id on the page context or null. Braces removed. */
export function entityIdOrNull(xrm: XRM | null): string | null {
    if (!xrm) return null
    const e = eaccess(xrm)
    if (e) return cleanId(e.getId())
    return null
}

const eaccess = R.pathOr(null, ["Page", "data", "entity"])

/**
 * After a save event, run actionToTake if ready returns ture. Uses polling.
 * Once actionToTake is run, the polling is removed. An exception thrown in
 * in ready is considered a false return.
 * @param xrm Xrm to attach to Xrm.Page.data.entity.addOnSave/removeOnSave
 * @param ready Return true if the condition to run actionToTake has been met.
 * @param actionToTake The action to take.
 * @param pollInterval The interval to poll that ready is true after the save has occurred.
 * @return A cancellable thunk.
 */
export function runAfterSave(xrm: XRM,
    ready: (x?: XRM) => boolean | null | undefined,
    actionToTake: (x?: XRM) => void,
    pollInterval: number = 500): () => void {
    let cancellable: any | null = null
    const onSaveHandler = (ctx) => {
        const keepChecking = () => {
            try {
                const fire = ready(xrm)
                if (fire) {
                    xrm.Page.data.entity.removeOnSave(onSaveHandler)
                    clearInterval(cancellable)
                    actionToTake(xrm)
                }
            } catch (e) {
                // do nothing
                console.log("Ready check failed", e)
            }
        }
        cancellable = setInterval(keepChecking, pollInterval)
    }
    xrm.Page.data.entity.addOnSave(onSaveHandler)
    return () => xrm.Page.data.entity.removeOnSave(onSaveHandler)
}

/** Render a null, which in react means no rendering. */
export const RenderNothing = () => null

/** Do nothing. */
export function noop() { }

/**
 * If cb is a function, return it, otherwise noop.
 * @param cb Callback
 */
export function callbackOrNoop(cb) {
    return typeof cb === "function" ? cb : noop
}

/**
 * If arg is an array, return the first element if it exists,
 * otherwise, return other.
 */
export function firstOrElse(arg, other) {
    arg = Array.isArray(arg) ? arg[0] : arg
    if (R.isNil(arg) && other) return other;
    else return arg;
}

/** Find the first undefined array element or return undefined. */
export function firstUndefined(...args) {
    if (!Array.isArray(args)) return undefined
    return args.find(a => typeof a !== "undefined")
}

/**
 * Execute fns with the same args in order until
 * `event.preventDefault()` is called. This is really
 * just a takeWhile and map where "event" is mutable state.
 */
export function composeEventHandlers(...fns) {
    return (event, ...args) => {
        fns.some(fn => { // does this test in array order???
            fn && fn(event, ...args)
            return event.defaultPrevented
        })
    }
}

/** Per downshift, (p)react. */
export function isDOMElement(el) {
    if (el) {
        if (el.nodeName) return typeof el.nodeName === "string"
        else return (typeof el.type === "string" ||
            typeof el.type === "function")
    }
    return false
}

/** Return true if its a number. */
export function isNumber(thing) {
    // eslint-disable-next-line no-self-compare
    return thing === thing && typeof thing === "number"
}

/**
 * Get props for (p)react.
 */
export function getElementProps(element) {
    return element.props || element.attributes
}

/**
 * Return the parent's Xrm from window.parent.Xrm or window.Xrm.
 * No check to see if Xrm.Page.data is present as that is deprecated.
* This is a strict value check, no async.
 *
 * @see getXrmForEntity
 */
export function getXrm(): XRM | null {
    return (window.parent.Xrm as XRM) || (window.Xrm as XRM)
}

/**
 * Return the global context from GetGlobalContext(), then 
 * Xrm.Utility.getGlobalContext() then Xrm.Page.context.
 * Throws Error if not found.
 *
 * @see https://msdn.microsoft.com/pt-pt/library/af74d417-1359-4eaa-9f87-5b33a8852e83(v=crm.7)
 */
export function getGlobalContext(): Xrm.GlobalContext {
    var errorMessage = "Context is not available.";
    if (typeof GetGlobalContext !== "undefined")
        { return GetGlobalContext() }
    else
        {
            if (typeof Xrm !== "undefined") {
                if(typeof Xrm.Utility !== "undefined" &&
                   typeof Xrm.Utility.getGlobalContext !== "undefined") {
                    return Xrm.Utility.getGlobalContext()
                }
                // Try this...
                return Xrm.Page.context
            }
            else { throw new Error(errorMessage) }
        }
}

/**
 * Walk the window chain looking for Xrm with Xrm.Page.data attribute being
 * non-null. Return null if not found. It will walk the window hierarchy
 * as well as test some well known locations of Xrm.
 *
 * @see getXrm
 */
export function getXrmForEntity(): XRM | null {
    const window: Window | null = walkParents({
        select: (w: Window) =>
            R.pathOr(false, ["Xrm", "Page", "data"], w)
    })
    if (window) return window.Xrm as XRM
    const maybeXrm = getXrm()
    if (R.pathOr(false, ["Page", "data"], maybeXrm)) return maybeXrm
    return null
}

/**
 * Run a thunk up the window chain. Return the last window visited if it meets
 * select criteria (if provided) or if select returns true for a particular
 * window.  Return null otherwise. Thunk is usually used for logging.
 */
export function walkParents({ thunk, select, max }: {
    thunk?: (w: Window) => void
    select: (w: Window) => boolean
    max?: number
}): Window | null {
    max = max || 10
    let current: Window | null = window // this window
    while (current && max > 0 && !select(current)) {
        if (thunk) thunk(current)
        max = max - 1
        if (current.parent === current)
            current = null
        else
            current = current.parent
    }
    if (select(current!)) return current
    return null
}

/**
 * Try to get entityid, userid, entity name, entity type code (number) from a
 * variety of places including the Xrm values and the URL parameters in the
 * document that the function is called from. Varibles that are found are
 * returned but if something is not found it is not returned.  If its a new
 * entity, obviously, the entityid will not be present.  Return an object with
 * {userId, entityId, entityName, entityTypeCode}. Note that entityTypeCode
 * is specific to an organization so do not use it if you can avoid it.
 *
 * Should typecode be number or string?
 */
export function getEntityInfo(xrm?: XRM | null): {
    userId?: string
    entityId?: string
    entityName?: string
    entityTypeCode?: number
} {
    const x = xrm || getXrmForEntity()
    const context = R.pathOr(null, ["Page", "context"], x)
    const entity = R.pathOr(null, ["Page", "data", "entity"], x) 
    const etn = getURLParameter("etn")
    const typename = getURLParameter("typename")
    const etc = context ? parseInt(context.getQueryStringParameters().etc) : null
    
    const eid = (entity && entity.getId()) || getURLParameter("id") || null
    const uid = (context && context.getUserId()) || null
    const ename = entity ? entity.getEntityName() :
                  (etn ? etn :
                   (typename ? typename : null))
    const tcode: number | null = etc

    const rval = {
        ...(uid ? { userId: cleanId(uid) } : {}),
        ...(eid ? { entityId: cleanId(eid) } : {}),
        ...(ename ? { entityName: ename } : {}),
        ...(tcode ? { entityTypeCode: tcode } : {})
    }
    return rval
}

/** 
 * Access page context to return form type.
 * @deprecated Use XRM members directly.
 */
export function getFormType(xrm: XRM): XrmEnum.FormType {
    const v = xrm.Page.ui.getFormType()
    // check range???
    return v as XrmEnum.FormType
}

/** If create form, checks formtype and whether there is an id. */
export function isCreateForm(xrm: XRM): boolean {
    return getFormType(xrm) === XrmEnum.FormType.Create || !eaccess(xrm).getId()
}


/**
 * Load scripts programmatically. The script is evaluated once loaded by the browser/host.
 */
export function loadScripts(scripts: Array<string>,
                            callback: () => void,
                            targetDoc: Document = document) {
    const loader = (src: string, handler: () => void) => {
        if(DEBUG) console.log("Programmatically loading: " + src)
        const script = targetDoc.createElement("script")
        script.src = src
        script.onload = () => {
            // remove onload handler??
            handler()
        }
        const head = targetDoc.getElementsByTagName("head")[0];
        (head || targetDoc.body).appendChild(script)
    }
    // Run on each script...
    (function run() {
        if (scripts.length > 0) {
            loader(scripts.shift()!, run)
        } else if (callback) callback()
    })()
}
