///////////////////////////////////////////////////////////////////////////////////////////////////
// Global definitions and built-in functions
///////////////////////////////////////////////////////////////////////////////////////////////////

load("nashorn:mozilla_compat.js");

importClass(Packages.jdk.nashorn.api.scripting.ScriptUtils);
importClass(java.beans.PropertyChangeListener)
importClass(java.lang.Thread)
importClass(java.awt.image.BufferedImage)
importClass(java.awt.Color)
importClass(java.awt.Font)
importClass(java.awt.Dimension)
importClass(java.io.File)

CommandSource = Java.type('ch.psi.pshell.core.CommandSource')
ContextListener = Java.type('ch.psi.pshell.core.ContextAdapter')
Context = Java.type('ch.psi.pshell.core.Context')
InlineDevice = Java.type('ch.psi.pshell.core.InlineDevice')
PlotDescriptor = Java.type('ch.psi.pshell.data.PlotDescriptor')
Table = Java.type('ch.psi.pshell.data.Table')
Device = Java.type('ch.psi.pshell.device.Device')
DeviceBase = Java.type('ch.psi.pshell.device.DeviceBase')
DeviceConfig = Java.type('ch.psi.pshell.device.DeviceConfig')
RegisterBase = Java.type('ch.psi.pshell.device.RegisterBase')
ProcessVariableBase = Java.type('ch.psi.pshell.device.ProcessVariableBase') 
ControlledVariableBase = Java.type('ch.psi.pshell.device.ControlledVariableBase') 
PositionerBase = Java.type('ch.psi.pshell.device.PositionerBase') 
MotorBase = Java.type('ch.psi.pshell.device.MotorBase') 
DiscretePositionerBase = Java.type('ch.psi.pshell.device.DiscretePositionerBase') 
MotorGroupBase= Java.type('ch.psi.pshell.device.MotorGroupBase') 
MotorGroupDiscretePositioner = Java.type('ch.psi.pshell.device.MotorGroupDiscretePositioner') 
ReadonlyRegisterBase = Java.type('ch.psi.pshell.device.ReadonlyRegisterBase') 
ReadonlyAsyncRegisterBase = Java.type('ch.psi.pshell.device.ReadonlyAsyncRegisterBase') 
Register = Java.type('ch.psi.pshell.device.Register') 
RegisterCache = Java.type('ch.psi.pshell.device.RegisterCache') 
ReadonlyRegisterArray = Java.type('ch.psi.pshell.device.ReadonlyRegister.ReadonlyRegisterArray') 
ReadonlyRegisterMatrix = Java.type('ch.psi.pshell.device.ReadonlyRegister.ReadonlyRegisterMatrix') 
DummyPositioner = Java.type('ch.psi.pshell.device.DummyPositioner') 
DummyMotor = Java.type('ch.psi.pshell.device.DummyMotor') 
DummyRegister = Java.type('ch.psi.pshell.device.DummyRegister') 
Timestamp = Java.type('ch.psi.pshell.device.Timestamp') 
Interlock = Java.type('ch.psi.pshell.device.Interlock') 
Readable = Java.type('ch.psi.pshell.device.Readable') 
ReadableArray = Java.type('ch.psi.pshell.device.Readable.ReadableArray') 
ReadableMatrix = Java.type('ch.psi.pshell.device.Readable.ReadableMatrix') 
ReadableCalibratedArray = Java.type('ch.psi.pshell.device.Readable.ReadableCalibratedArray') 
ReadableCalibratedMatrix = Java.type('ch.psi.pshell.device.Readable.ReadableCalibratedMatrix') 
ArrayCalibration = Java.type('ch.psi.pshell.device.ArrayCalibration') 
MatrixCalibration = Java.type('ch.psi.pshell.device.MatrixCalibration') 
Writable = Java.type('ch.psi.pshell.device.Writable') 
WritableArray = Java.type('ch.psi.pshell.device.Writable.WritableArray') 
Stoppable = Java.type('ch.psi.pshell.device.Stoppable') 
Averager = Java.type('ch.psi.pshell.device.Averager') 
ArrayAverager = Java.type('ch.psi.pshell.device.ArrayAverager') 
Delta = Java.type('ch.psi.pshell.device.Delta') 
DeviceListener = Java.type('ch.psi.pshell.device.DeviceAdapter') 
ReadbackDeviceListener = Java.type('ch.psi.pshell.device.ReadbackDeviceAdapter') 
MotorListener = Java.type('ch.psi.pshell.device.MotorAdapter') 
MoveMode  = Java.type('ch.psi.pshell.device.MoveMode') 
SettlingCondition = Java.type('ch.psi.pshell.device.SettlingCondition')
Epics = Java.type('ch.psi.pshell.epics.Epics') 
EpicsScan = Java.type('ch.psi.pshell.epics.EpicsScan') 
ChannelSettlingCondition = Java.type('ch.psi.pshell.epics.ChannelSettlingCondition')
Source = Java.type('ch.psi.pshell.imaging.Source') 
SourceBase = Java.type('ch.psi.pshell.imaging.SourceBase') 
DirectSource = Java.type('ch.psi.pshell.imaging.DirectSource') 
RegisterMatrixSource = Java.type('ch.psi.pshell.imaging.RegisterMatrixSource') 
LinePlotSeries = Java.type('ch.psi.pshell.plot.LinePlotSeries') 
LinePlotErrorSeries = Java.type('ch.psi.pshell.plot.LinePlotErrorSeries') 
MatrixPlotSeries = Java.type('ch.psi.pshell.plot.MatrixPlotSeries') 
AxisId = Java.type('ch.psi.pshell.plot.Plot.AxisId')
LinePlotStyle = Java.type('ch.psi.pshell.plot.LinePlot.Style')
LineScan = Java.type('ch.psi.pshell.scan.LineScan')
ContinuousScan = Java.type('ch.psi.pshell.scan.ContinuousScan')
AreaScan = Java.type('ch.psi.pshell.scan.AreaScan')
VectorScan = Java.type('ch.psi.pshell.scan.VectorScan')
ManualScan = Java.type('ch.psi.pshell.scan.ManualScan')
RegionScan = Java.type('ch.psi.pshell.scan.RegionScan')
HardwareScan = Java.type('ch.psi.pshell.scan.HardwareScan')
TimeScan = Java.type('ch.psi.pshell.scan.TimeScan')
MonitorScan = Java.type('ch.psi.pshell.scan.MonitorScan')
BinarySearch = Java.type('ch.psi.pshell.scan.BinarySearch')
HillClimbingSearch = Java.type('ch.psi.pshell.scan.HillClimbingSearch')
ScanResult = Java.type('ch.psi.pshell.scan.ScanResult')
BsScan = Java.type('ch.psi.pshell.bs.BsScan')
Stream = Java.type('ch.psi.pshell.bs.Stream') 
Preference = Java.type('ch.psi.pshell.scripting.ViewPreference')
Threading =  Java.type('ch.psi.utils.Threading')
Convert =  Java.type('ch.psi.utils.Convert')
Arr =  Java.type('ch.psi.utils.Arr')
Chrono =  Java.type('ch.psi.utils.Chrono')
State =  Java.type('ch.psi.utils.State')
ScriptingUtils = Java.type('ch.psi.pshell.scripting.ScriptUtils')


function get_context() {
    return Context.getInstance()
}

function on_command_started(info) {
}

function on_command_finished(info) {
}

///////////////////////////////////////////////////////////////////////////////////////////////////
// Type conversion and checking
///////////////////////////////////////////////////////////////////////////////////////////////////

function is_defined(obj) {
    return (typeof obj != 'undefined')
}

function get_rank(obj) {    
    if (typeof obj == 'string') return 0
    var rank = 0;
    while ((obj != null) && (typeof obj != 'string')  &&(obj[0] != null)) {
        rank++;
        obj = obj[0];
    }
    return rank;
}

function assert(condition, message) {
    if (!condition) {
        throw message || "Assertion failed";
    }
}

//TODO: this is wrong: returns false for 1.0
function is_float(n) {
    if (n == null){
        return false;
    }
    if (get_rank(n) == 1) {
        for (var i = 0; i < n.length; i++) {
            if (is_float(n[i])) {
                return true;
            }
        }
        return false;
    }
    //return n === Number(n) && n % 1 !== 0;
    return (n.class.simpleName == "Float"  ||  n.class.simpleName == "Double")
}

function is_array(obj) {
    //return get_rank(obj) == 0
    //return !(obj.length == undefined)
    //return obj.constructor === Array
    return Array.isArray(obj)
}


function is_java_array(obj){
    return !Array.isArray(obj)  && obj.class.isArray()	
}

function is_java_list(obj){
    return obj instanceof java.util.List	
}

function to_array(obj, type) {
    //var javaObjectArray = Java.to(obj);

    if (obj == null) {
        return null;
    }
    if (!is_defined(type)) {
        //If undefined type, convert to a JavaScript array
        if (is_array(obj)){
            return obj
        }
        return  Java.from(obj);
    }

    if (obj instanceof java.util.List){
        obj = to_array(obj)
    }

    var rank = get_rank(obj);
    if (rank == 0) {
        rank = 1;
        obj = [obj]
    }

    //Custom class
    if (!ScriptingUtils.isStandardType(type)) {
        var ret = java.lang.reflect.Array.newInstance(java.lang.Class.forName(type), obj.length)
        for (var i = 0; i < obj.length; i++) {
            ret[i] = obj[i]
        }
        return ret;
    }

    if (is_array(obj) && (type=='o')){
        for (var i = 0; i < obj.length; i++) {
            if (is_array(obj[i])){            	
                obj[i] = to_array(obj[i], type)                

            }
        }        
    }    
    
    var name = ScriptingUtils.getType(type).getName()
    if (type[0] != "[") {
        type = "[" + type;
        name = ScriptingUtils.getType(type).getName();
        //Type 'o' always create a 1d array
        if (type!='[o'){
            for (var i = 1; i < rank; i++) {
                name = "[" + name;               
            }
        }
    }

    return ScriptUtils.convert(obj, java.lang.Class.forName(name))
}


///////////////////////////////////////////////////////////////////////////////////////////////////
// Standard scan commands
///////////////////////////////////////////////////////////////////////////////////////////////////


function lscan(writables, readables, start, end, steps, latency, relative, passes, zigzag, before_read, after_read, title) {
    /*
     Line Scan: positioners change together, linearly from start to end positions.
     
     Args:
        writables(list of Writable): Positioners set on each step.
        readables(list of Readable): Sensors to be sampled on each step.
        start(list of float): start positions of writables.
        end(list of float): final positions of writables.
        steps(int or float or list of float): number of scan steps (int) or step size (float).
        relative (bool, optional): if true, start and end positions are relative to 
        current at start of the scan
        latency(float, optional): sleep time in each step before readout, defaults to 0.0.     
        passes(int, optional): number of passes
        zigzag(bool, optional): if true writables invert direction on each pass.    
        before_read (function, optional): callback on each step, before each readout. Callback may have as 
        optional parameters list of positions.
        after_read (function, optional): callback on each step, after each readout. Callback may have as 
        optional parameters a ScanRecord object. 
        title(str, optional): plotting window name.
     
     Returns:
         ScanResult object.
     */
    if (!is_defined(latency))    latency = 0.0;
    if (!is_defined(passes))    passes = 1;
    if (!is_defined(zigzag))    zigzag = false;
    if (!is_defined(relative))   relative = false;
    if (!is_defined(title))    title = null;
    
    var latency_ms = latency * 1000
    var writables = to_array(string_to_obj(writables), "ch.psi.pshell.device.Writable")
    var readables = to_array(string_to_obj(readables), "ch.psi.pshell.device.Readable")
    var start = to_array(start, 'd')
    var end = to_array(end, 'd')
    var steps = is_float(steps) ? to_array(steps, 'd') : steps
    //var scan = new LineScan(writables, readables, start, end, steps, relative, latency_ms)
    var scanClass = Java.extend(LineScan)
    var scan = new scanClass(writables, readables, start, end, steps, relative, latency_ms, passes, zigzag) {
        onBeforeReadout: function (pos) {
            if (is_defined(before_read))
                before_read(pos , scan)
        },
        onAfterReadout: function (record) {
            if (is_defined(after_read))
                after_read(record , scan)
        }
    }
    scan.setPlotTitle(title)
    scan.start()
    return scan.getResult()
}


function vscan(writables, readables, vector, line, latency, relative, passes, zigzag, before_read, after_read, title) {
    /*
     Vector Scan: positioners change following values provided in a vector.
     
     Args:
        writables(list of Writable): Positioners set on each step.
        readables(list of Readable): Sensors to be sampled on each step.
        vector(list of list of float): table of positioner values.
        line (bool, optional): if true, processs as line scan (1d)
        relative (bool, optional): if true, start and end positions are relative to current at 
        start of the scan
        latency(float, optional): sleep time in each step before readout, defaults to 0.0.        
        passes(int, optional): number of passes
        zigzag(bool, optional): if true writables invert direction on each pass.
        before_read (function, optional): callback on each step, before each readout.
        after_read (function, optional): callback on each step, after each readout.
        title(str, optional): plotting window name.
     
     Returns:
         ScanResult object.
     */
    if (!is_defined(line))   line = false;
    if (!is_defined(latency))    latency = 0.0;
    if (!is_defined(passes))    passes = 1;
    if (!is_defined(zigzag))    zigzag = false;
    if (!is_defined(relative))   relative = false;
    if (!is_defined(title))    title = null;
    var latency_ms = latency * 1000
    var writables = to_array(string_to_obj(writables), "ch.psi.pshell.device.Writable")
    var readables = to_array(string_to_obj(readables), "ch.psi.pshell.device.Readable")
    

    if (vector.length == 0)
        vector.push([]);
    else if (!is_array(vector[0])) {    	
        var aux = []
        for (var i = 0; i < vector.length; i++)
            aux.push([vector[i]]);
        var vector = aux
    }

    vector = to_array(vector, 'd')
    var scanClass = Java.extend(VectorScan)
    
    var scan = new scanClass(writables, readables, vector, line, relative, latency_ms, passes, zigzag) {
        onBeforeReadout: function (pos) {
            if (is_defined(before_read))
                before_read(pos , scan)
        },
        onAfterReadout: function (record) {
            if (is_defined(after_read))
                after_read(record, scan)
        }
    }
    scan.setPlotTitle(title)
    scan.start()
    return scan.getResult()
}

function ascan(writables, readables, start, end, steps, latency, relative, passes, zigzag, before_read, after_read, title) {
    /*      
     Area Scan: multi-dimentional scan, each positioner is a dimention.
     
     Args:
        writables(list of Writable): Positioners set on each step.
        readables(list of Readable): Sensors to be sampled on each step.
        start(list of float): start positions of writables.
        end(list of float): final positions of writables.
        steps(list of int or list of float): number of scan steps (int) or step size (float).
        relative (bool, optional): if true, start and end positions are relative to current at 
        start of the scan
        latency(float, optional): sleep time in each step before readout, defaults to 0.0.
        passes(int, optional): number of passes
        zigzag (bool, optional): if true writables invert direction on each row.
        before_read (function, optional): callback on each step, before each readout.
        after_read (function, optional): callback on each step, after each readout.
        title(str, optional): plotting window name.
     
     Returns:
         ScanResult object.
     */
    if (!is_defined(latency))    latency = 0.0;
    if (!is_defined(relative))   relative = false;
    if (!is_defined(passes))    passes = 1;
    if (!is_defined(zigzag))    zigzag = false;
    if (!is_defined(title))    title = null;

    var latency_ms = latency * 1000
    var writables = to_array(string_to_obj(writables), "ch.psi.pshell.device.Writable")
    var readables = to_array(string_to_obj(readables), "ch.psi.pshell.device.Readable")
    var start = to_array(start, 'd')
    var end = to_array(end, 'd')
    var steps = is_float(steps) ? to_array(steps, 'd') : to_array(steps, 'i') 
    var scanClass = Java.extend(AreaScan)
    var scan = new scanClass(writables, readables, start, end, steps, relative, latency_ms, passes, zigzag) {
        onBeforeReadout: function (pos) {
            if (is_defined(before_read))
                before_read(pos , scan)
        },
        onAfterReadout: function (record) {
            if (is_defined(after_read))
                after_read(record, scan)
        }
    }
    scan.setPlotTitle(title)
    scan.start()
    return scan.getResult()
}

function  rscan(writable, readables, regions, latency, relative, passes, zigzag, before_read, after_read, title) {
    /*
    Region Scan: positioner scanned linearly, from start to end positions, in multiple regions.

    Args:
        writable(Writable): Positioner set on each step, for each region.
        readables(list of Readable): Sensors to be sampled on each step.
        regions (list of tuples (float,float, int)   or (float,float, float)): each tuple define a scan region
                                (start, stop, steps) or (start, stop, step_size)                                  
        relative (bool, optional): if true, start and end positions are relative to 
            current at start of the scan
        latency(float, optional): settling time for each step before readout, defaults to 0.0.
        passes(int, optional): number of passes
        zigzag(bool, optional): if true writable invert direction on each pass.
        before_read (function, optional): callback on each step, before each readout. Callback may have as 
            optional parameters list of positions.
        after_read (function, optional): callback on each step, after each readout. Callback may have as 
            optional parameters a ScanRecord object. 
        title(str, optional): plotting window name.

    Returns:
        ScanResult object.
     */
    
    if (!is_defined(latency))    latency = 0.0;
    if (!is_defined(passes))    passes = 1;
    if (!is_defined(zigzag))    zigzag = false;
    if (!is_defined(relative))   relative = false;
    if (!is_defined(title))    title = null;
    
    var latency_ms = latency * 1000
    var writable = string_to_obj(writable)
    var readables = to_array(string_to_obj(readables), "ch.psi.pshell.device.Readable")

    var start = []
    var end = []
    var steps = []
    
    for (var region in regions) {
        start.push(regions[region][0])
        end.push(regions[region][1])
        steps.push(regions[region][2])
    }

    var start = to_array(start, 'd')
    var end = to_array(end, 'd')
    var steps = is_float(steps) ? to_array(steps, 'd') : to_array(steps, 'i')  
    

    var scanClass = Java.extend(RegionScan)
    var scan = new scanClass(writable, readables, start, end, steps, relative, latency_ms, passes, zigzag) {
        onBeforeReadout: function (pos) {
            if (is_defined(before_read))
                before_read(pos , scan)
        },
        onAfterReadout: function (record) {
            if (is_defined(after_read))
                after_read(record, scan)
        }
    }
    scan.setPlotTitle(title)
    scan.start()
    return scan.getResult()
 }


function  cscan(writables, readables, start, end, steps, latency, time, relative, passes, zigzag, before_read, after_read, title) {
    /*
     Continuous Scan: positioner change continuously from start to end position and readables are sampled on the fly.
     
     Args:
        writable(Speedable or list of Motor): A positioner with a  getSpeed method or 
        a list of motors.
        readables(list of Readable): Sensors to be sampled on each step.
        start(float or list of float): start positions of writables.
        end(float or list of float): final positions of writabless.
        steps(int or float or list of float): number of scan steps (int) or step size (float).
        time = null
        time (float, seconds): if not null then writables is Motor array and speeds are 
        set according to time.
        relative (bool, optional): if true, start and end positions are relative to 
        current at start of the scan
        latency(float, optional): sleep time in each step before readout, defaults to 0.0.
        before_read (function, optional): callback on each step, before each readout. 
        Callback may have as optional parameters list of positions.
        after_read (function, optional): callback on each step, after each readout. 
        Callback may have as optional parameters a ScanRecord object. 
        title(str, optional): plotting window name.
     
     Returns:
        ScanResult object.
     
     */
    if (!is_defined(latency))    latency = 0.0;
    if (!is_defined(time))   time = null;
    if (!is_defined(relative))   relative = false;
    if (!is_defined(passes))    passes = 1;
    if (!is_defined(zigzag))    zigzag = false;    
    if (!is_defined(title))    title = null;

    var latency_ms = latency * 1000    
    var readables = to_array(string_to_obj(readables), "ch.psi.pshell.device.Readable")    
    var scanClass = Java.extend(ContinuousScan)
    //A single Writable with fixed speed
    if (time == null) {    	  
        var scan = new scanClass(writables, readables, start, end, steps, relative, latency_ms, passes, zigzag){
            onBeforeReadout: function (pos) {
                if (is_defined(before_read))
                    before_read(pos , scan)
            },
            onAfterReadout: function (record) {
                if (is_defined(after_read))
                    after_read(record, scan)
            }
            	
        }                
    }
    else {
    	var writables = to_array(string_to_obj(writables), "ch.psi.pshell.device.ControlledSpeedable")
        //A set of Writables with speed configurable
        var start = to_array(start, 'd')
        var end = to_array(end, 'd')    
        if ((steps.length!=null) && is_float(steps)){
        	steps = to_array(steps, 'd')
        }
        var scan = new ContinuousScan(writables, readables, start, end, steps, time, relative, latency_ms, passes, zigzag){
            onBeforeReadout: function (pos) {
                if (is_defined(before_read))
                    before_read(pos , scan)
            },
            onAfterReadout: function (record) {
                if (is_defined(after_read))
                    after_read(record, scan)
            }            	
        }                
    }
    scan.setPlotTitle(title)
    scan.start()
    return scan.getResult()
}

function hscan(config, writable, readables, start, end, steps, passes, zigzag, before_stream, after_stream, after_read, title) {
    /*
    Hardware Scan: values sampled by external hardware and received asynchronously.

    Args:
        config(dict): Configuration of the hardware scan. The "class" key provides the implementation class.
                      Other keys are implementation specific.
        writable(Writable): A positioner appropriated to the hardware scan type.
        readables(list of Readable): Sensors appropriated to the hardware scan type.
        start(float): start positions of writable.
        end(float): final positions of writables.
        steps(int or float): number of scan steps (int) or step size (float).
        before_stream (function, optional): callback before just before starting positioner move. 
        after_stream (function, optional): callback before just after stopping positioner move. 
        after_read (function, optional): callback on each readout. 
                    Callback may have as optional parameters a ScanRecord object. 
        title(str, optional): plotting window name.

    Returns:
        ScanResult object.

    */    
    if (!is_defined(passes))    passes = 1;
    if (!is_defined(zigzag))    zigzag = false;    
    if (!is_defined(title))    title = null;    
    //var scan = HardwareScan.newScan(config, writable,readables, start, end , steps, passes, zigzag);
    var scanClass = Java.extend(HardwareScan)
    var scan = new scanClass(config, writable, readables, start, end, steps, passes, zigzag) {
        onAfterReadout: function (record) {
            if (is_defined(after_read))
                after_read(record, scan)
        },
        onBeforeStream: function (current_pass) {
            if (is_defined(before_stream))
                before_stream(current_pass)
        },
        onAfterStream: function (current_pass) {
            if (is_defined(after_stream))
                after_stream(current_pass)
        },
    }
    scan.setPlotTitle(title)
    scan.start()
    return scan.getResult()
}

function bscan(stream, records, timeout, before_read, after_read, title) {
    /*
    BS Scan: records all values in a beam synchronous stream.

    Args:
        stream(Stream): stream object
        records(int): number of records to store
        before_read (function, optional): callback on each step, before each readout. 
                    Callback may have as optional parameters list of positions.
        after_read (function, optional): callback on each step, after each readout. 
                    Callback may have as optional parameters a ScanRecord object. 
        title(str, optional): plotting window name.

    Returns:
        ScanResult object.

    */
    if (!is_defined(title))    title = null;
    if (!is_defined(timeout))    timeout = -1;
    var timeout_ms = timeout * 1000

    var readables = stream
    if (!is_array(stream)){
        readables = string_to_obj(stream)
    }
    var scanClass = Java.extend(BsScan)
    var scan = new scanClass(readables,records, timeout_ms) {
        onBeforeReadout: function (pos) {
            if (is_defined(before_read))
                before_read(pos , scan)
        },
        onAfterReadout: function (record) {
            if (is_defined(after_read))
                after_read(record, scan)
        }            	
    }                    
    scan.setPlotTitle(title)
    scan.start()
    return scan.getResult()
}

function  tscan(readables, points, interval, title, before_read, after_read) {
    /* 
     Time Scan: sensors are sampled in fixed time intervals.
     
     Args:
        readables(list of Readable): Sensors to be sampled on each step.
        points(int): number of samples.
        interval(float): time interval between readouts.
        before_read (function, optional): callback on each step, before each readout.
        after_read (function, optional): callback on each step, after each readout.
        title(str, optional): plotting window name.
     
     Returns:
         ScanResult object.
     */
    if (!is_defined(title))    title = null;


    var interval_ms = interval * 1000
    var readables = to_array(string_to_obj(readables), "ch.psi.pshell.device.Readable")
    var scanClass = Java.extend(TimeScan)
    var scan = new scanClass(readables, points, interval_ms) {
        onBeforeReadout: function (pos) {
            if (is_defined(before_read))
                before_read(pos, scan)
        },
        onAfterReadout: function (record) {
            if (is_defined(after_read))
                after_read(record, scan)
        }
    }
    scan.setPlotTitle(title)
    scan.start()
    return scan.getResult()
}

function  mscan(trigger, readables, points, timeout, async, take_initial, before_read, after_read, title) {
    /* 
    Monitor Scan: sensors are sampled in timestamp change event of the trigger.

    Args:
        trigger(Device or list of Device): Source of the sampling triggering.
        readables(list of Readable): Sensors to be sampled on each step.
                                     If  trigger has cache and is included in readables, it is not read 
                                     for each step, but the change event value is used.
        points(int): number of samples.
        timeout(float, optional): maximum scan time in seconds. 
        async(bool, optional): if True then records are sampled and stored on event change callback. Enforce 
                               reading only cached values of sensors.
                               If False, the scan execution loop waits for trigger cache update. Do not make 
                               cache only access, but may loose change events.
        take_initial(bool, optional): if True include current values as first record (before first trigger).
        before_read (function, optional): callback on each step, before each readout.
        after_read (function, optional): callback on each step, after each readout.
        title(str, optional): plotting window name.

    Returns:
        ScanResult object.
    */
    if (!is_defined(timeout))    timeout = -1;
    if (!is_defined(async))    async = true;
    if (!is_defined(take_initial))    take_initial = false;
    if (!is_defined(title))    title = null;

    var timeout_ms = timeout * 1000
    var trigger = string_to_obj(trigger)
    var readables = to_array(string_to_obj(readables), "ch.psi.pshell.device.Readable")
    var scanClass = Java.extend(MonitorScan)
    var scan = new scanClass(trigger, readables, points, timeout_ms, async, take_initial) {
        onBeforeReadout: function (pos) {
            if (is_defined(before_read))
                before_read(pos, scan)
        },
        onAfterReadout: function (record) {
            if (is_defined(after_read))
                after_read(record, scan)
        }
    }
    scan.setPlotTitle(title)
    scan.start()
    return scan.getResult()
}

function escan(name, title) {
    /*
     Epics Scan: execute an Epics Scan Record.
     Args:
        name(str): Name of scan record.
        title(str, optional): plotting window name.
     
     Returns:
         ScanResult object.
     */

    if (!is_defined(title))    title = null;
    var scan = new EpicsScan(name)
    scan.setPlotTitle(title)
    scan.start()
    return scan.getResult()
}

function bsearch(writables, readable, start, end, steps, maximum, strategy, latency, relative, before_read, after_read, title){
    /*
    Binary search: searches writables in a binary search fashion to find a local maximum for the readable.

    Args:
        writables(list of Writable): Positioners set on each step.
        readable(Readable): Sensor to be sampled.
        start(list of float): start positions of writables.
        end(list of float): final positions of writables.
        steps(float or list of float): resolution of search for each writable.
        maximum (bool , optional): if True (default) search maximum, otherwise minimum.
        strategy (str , optional): "Normal": starts search midway to scan range and advance in the best direction.
                                             Uses orthogonal neighborhood (4-neighborhood for 2d)
                                   "Boundary": starts search on scan range.                                              
                                   "FullNeighborhood": Uses complete neighborhood (8-neighborhood for 2d)
        
        latency(float, optional): settling time for each step before readout, defaults to 0.0.
        relative (bool, optional): if true, start and end positions are relative to current at 
            start of the scan
        before_read (function, optional): callback on each step, before each readout.
        after_read (function, optional): callback on each step, after each readout.
        title(str, optional): plotting window name.

    Returns:
        SearchResult object.

    */
    if (!is_defined(maximum))   maximum = true;
    if (!is_defined(strategy))   strategy = "Normal";
    if (!is_defined(latency))    latency = 0.0;
    if (!is_defined(relative))   relative = false;
    if (!is_defined(title))    title = null;

    var latency_ms = latency * 1000    
    var writables = to_array(string_to_obj(writables), "ch.psi.pshell.device.Writable")
    var readable = string_to_obj(readable)
    var start = to_array(start, 'd')
    var end = to_array(end, 'd')
    var steps = to_array(steps, 'd')
    strategy = BinarySearch.Strategy.valueOf(strategy)
    var scanClass = Java.extend(BinarySearch)
    var scan = new scanClass(writables,readable, start, end , steps, maximum, strategy, relative, latency_ms) {
        onBeforeReadout: function (pos) {
            if (is_defined(before_read))
                before_read(pos , scan)
        },
        onAfterReadout: function (record) {
            if (is_defined(after_read))
                after_read(record, scan)
        }
    }
    scan.setPlotTitle(title)
    scan.start()
    return scan.getResult()
}

function hsearch(writables, readable, range_min,  range_max, initial_step, resolution, noise_filtering_steps, maximum, latency, relative, before_read, after_read, title){
    /*
    Hill Climbing search: searches writables in decreasing steps to find a local maximum for the readable.
    Args:
        writables(list of Writable): Positioners set on each step.
        readable(Readable): Sensor to be sampled.
        range_min(list of float): minimum positions of writables.
        range_max(list of float): maximum positions of writables.
        initial_step(float or list of float):initial step size for for each writable.
        resolution(float or list of float): resolution of search for each writable (minimum step size).
        noise_filtering_steps(int): number of aditional steps to filter noise
        maximum (bool , optional): if True (default) search maximum, otherwise minimum.
        latency(float, optional): settling time for each step before readout, defaults to 0.0.
        relative (bool, optional): if true, range_min and range_max positions are relative to current at 
            start of the scan
        before_read (function, optional): callback on each step, before each readout.
        after_read (function, optional): callback on each step, after each readout.
        title(str, optional): plotting window name.

    Returns:
        SearchResult object.

    */    
    if (!is_defined(maximum))   maximum = true;
    if (!is_defined(noise_filtering_steps))   noise_filtering_steps = 1;
    if (!is_defined(latency))    latency = 0.0;
    if (!is_defined(relative))   relative = false;
    if (!is_defined(title))    title = null;

    var latency_ms = latency * 1000   
    var latency_ms = latency * 1000    
    var readable = string_to_obj(readable)
    var range_min = to_array(range_min, 'd')
    var range_max = to_array(range_max, 'd')
    var initial_step = to_array(initial_step, 'd')
    var resolution = to_array(resolution, 'd')
    var scanClass = Java.extend(HillClimbingSearch)
    var scan = new scanClass(writables,readable, range_min, range_max , initial_step, resolution, noise_filtering_steps, maximum, relative, latency_ms) {
        onBeforeReadout: function (pos) {
            if (is_defined(before_read))
                before_read(pos , scan)
        },
        onAfterReadout: function (record) {
            if (is_defined(after_read))
                after_read(record, scan)
        }
    }
    scan.setPlotTitle(title)
    scan.start()
    return scan.getResult()
}

///////////////////////////////////////////////////////////////////////////////////////////////////
// Data Plotting
///////////////////////////////////////////////////////////////////////////////////////////////////
function plot(data, name, xdata, ydata, title) {
    /*Request one or multiple plots of user data (1d, 2d or 3d)
     
     Args:
        data: array or list of values. For multiple plots, array of arrays or lists of values.
        name(str or list of str, optional): Plot name or list of names (if multiple plots). 
        xdata: array or list of values. For multiple plots, array of arrays or lists of values.
        ydata: array or list of values. For multiple plots, array of arrays or lists of values.
        title(str, optional): plotting window name.
     
     Returns:
         ArrayList of Plot objects.
     
     */
    if (!is_defined(name))   name = null;
    if (!is_defined(xdata))  xdata = null;
    if (!is_defined(ydata))  ydata = null;
    if (!is_defined(title))  title = null;

    if (name != null) {
        if( typeof name === 'string' ) {
            name = [name]
            data =  [data]
        }
        if (name.length == 0) {
            name = null;
        } else {
            if (!data) {
                data = []
                for (var n in name) {
                    data.push([]);
                }
            }
        }
        var plots = java.lang.reflect.Array.newInstance(java.lang.Class.forName("ch.psi.pshell.data.PlotDescriptor"), data.length)
        for (var i = 0; i < data.length; i++) {
            var plotName = name ? name[i] : null;
            var x = xdata;
            if (x && x.length && x[0].length) { 
                x = x[i];
            }
            var y = ydata
            if (y && y.length && x[0].length) { 
                y = y[i];
            }
            plots[i] = new PlotDescriptor(plotName, to_array(data[i], 'd'), to_array(x, 'd'), to_array(y, 'd'));
        }
        return get_context().plot(plots, title);
    } else {
        var plot = new PlotDescriptor(name, to_array(data, 'd'), to_array(xdata, 'd'), to_array(ydata, 'd'));

        return get_context().plot(plot, title);
    }

}

function get_plots(title){
    /*
    Return all current plots in the plotting window given by 'title'.

    Args:
        title(str, optional): plotting window name.

    Returns:
        ArrayList of Plot objects.

    */
    if (!is_defined(title))
        title = null;
    return get_context().getPlots(title)
}

function get_plot_snapshots(title, file_type, temp_path){
    /*
    Returns list with file names of plots snapshots from a plotting context.

    Args:
        title(str, optional): plotting window name.
        file_type(str, optional): "png", "jpg", "bmp" or "gif"
        temp_path(str, optional): path where the files will be generated.

    Returns:
        list of strings

    */
    if (!is_defined(title))   title = null;
    if (!is_defined(file_type))   file_type = "png";   
    if (!is_defined(temp_path))   temp_path = get_context().setup.getContextPath();
    sleep(0.1) //Give some time to plot to be finished - it is not sync  with acquisition
    var ret = []
    var plots = get_plots(title)
    for (p in plots){
        var file_name = new File(temp_path + "/" + plots[p].getTitle() + "." + file_type).getCanonicalPath()
        plots[p].saveSnapshot(file_name , file_type)
        ret.push(file_name)
    }
    return ret
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// Data file access
///////////////////////////////////////////////////////////////////////////////////////////////////

function load_data(path, index, shape) {
    /*
     Read data from the current persistence context or from data files.
     
     Args:
        path(str): Path to group or dataset relative to the persistence context root.
                   If in the format 'root|path' then read from path given by 'root'.
        index(int or listr, optional): 
                if integer, data depth (used for 3D datasets returning a 2d matrix)
                If a list, specifies the full coordinate for multidimensional datasets.
     Returns:
         Data array
     
     */
    if (!is_defined(index))
        index = 0;
    if (!is_defined(shape))
        shape = null;

    if ((shape!=null) && (is_array(index)))
        var slice = get_context().dataManager.getData(path, index, shape)
    else
        var slice = get_context().dataManager.getData(path, index)
    return slice.sliceData
}

function get_attributes(path) {
    /*
     Get the attributes from the current persistence context or from data files.
     
     Args:
        path(str): Path to group or dataset relative to the current persistence context root.
                  If in the format 'root|path' then read from path given by 'root'.
     Returns:
         Dictionary
     
     */
    return get_context().dataManager.getAttributes(path)
}

function save_dataset(path, data, type, features) {
    /*
     Save data into a dataset within the current persistence context.
     
     Args:
        path(str): Path to dataset relative to the current persistence context root.
        type(str, optional): array type 'b' = byte, 'h' = short, 'i' = int, 'l' = long,  'f' = float, 
                              'd' = double, 'c' = char, 's' = String,  'o' = Object 
                   default: 'd' (convert data to array of doubles)
        data (array or list): data to be saved
        features(dictionary, optional): See create_dataset.
     Returns:
        Dictionary
     
     */
    if (!is_defined(type))
        type = 'd'
    if (is_array(data))
        data = to_array(data, type)    
    if (!is_defined(features))
        features = null;
    get_context().dataManager.setDataset(path, data, features)
}

function create_group(path) {
    /*
    Create an empty dataset within the current persistence context.

    Args:
        path(str): Path to group relative to the current persistence context root.
    Returns:
        null
     
     */
    get_context().dataManager.createGroup(path)
}


function create_dataset(path, type, unsigned, dimensions, features) {
    /*
    Create an empty dataset within the current persistence context.

    Args:
        path(str): Path to dataset relative to the current persistence context root.
        type(str): array type 'b' = byte, 'h' = short, 'i' = int, 'l' = long,  'f' = float,
                              'd' = double, 'c' = char, 's' = String,  'o' = Object
        unsigned(boolean, optional): create a dataset of unsigned type.
        dimensions(tuple of int, optional): a 0 value means variable length in that dimension.
        features(dictionary, optional): storage features for the dataset, provider specific.
            Keys for HDF5: "layout": "compact", "contiguous" or "chunked"
                           "compression": True, "max" or deflation level from 1 to 9
                           "shuffle": Byte shuffle before compressing.
                           "chunk": tuple, setting the chunk size
            Default: No compression, contiguous for fixed size arrays, chunked for variable size, compact for scalars.
     Returns:
         null
     
     */
    if (!is_defined(unsigned))
        unsigned = false;
    if (!is_defined(dimensions))
        dimensions = null;
    if (!is_defined(type))
        type = null;    
    if (!is_defined(features))
        features = null;
    get_context().dataManager.createDataset(path, ScriptingUtils.getType(type), unsigned, dimensions, features)
}

function create_table(path, names, types, lengths, features) {
    /*
     Create an empty table (dataset of compound type) within the current persistence context.

     Args:
        path(str): Path to dataset relative to the current persistence context root.
        names(list of strings): name of each column
        types(array of str):  'b' = byte, 'h' = short, 'i' = int, 'l' = long,  'f' = float, 
        'd' = double, 'c' = char, 's' = String,  'o' = Object 
        Note:A '[' prefix on type name indicates an array type.
        lengths(list of int): the array length for each columns(0 for scalar types).
        features(dictionary, optional): See create_dataset.
     Returns:
         null
     
     */
    if (!is_defined(types))
        types = null;
    if (!is_defined(lengths))
        lengths = null;
    if (!is_defined(features))
        features = null;
    var new_types = null
    if (types != null) {
        new_types = []
        for (var i = 0; i < types.length; i++){
            new_types.push(ScriptingUtils.getType(types[i]))
        }
    }
    get_context().dataManager.createDataset(path, names, new_types, lengths, features)
}

function append_dataset(path, data, index, type, shape) {
    /*
     Append data to dataset.

     Args:
        path(str): Path to dataset relative to the current persistence context root.
        data(number or array or list): name of each column.
        index(int or list, optional): if set then add the data in a specific position in the dataset.
                If integer is the index in an array (data must be 1 order lower than dataset)
                If a list, specifies the full coordinate for multidimensional datasets.
        type(str, optional): array type 'b' = byte, 'h' = short, 'i' = int, 'l' = long,  'f' = float, 
                              'd' = double, 'c' = char, 's' = String,  'o' = Object 
                   default: 'd' (convert data to array of doubles)
        shape(list, optional): only valid if index is a list, provides the shape of the data array.
                In this case data must be a one-dimensional array.
     Returns:
        null
     
     */
    if (!is_defined(index))
        index = null;
    if (!is_defined(type))
        type = 'd';
    if (!is_defined(shape))
        shape = null;
    if (is_array(data)){
        data = to_array(data, type)
    }
    if (index == null)
        get_context().dataManager.appendItem(path, data);
    else {
        if (is_array(index))
            get_context().dataManager.setItem(path, data, index, shape);
        else
            get_context().dataManager.setItem(path, data, index);
    }
}

function append_table(path, data) {
    /*
     Append data to a table (dataset of compound type) 

     Args:
        path(str): Path to dataset relative to the current persistence context root.
        data(list): List of valus for each column of the table. Array types can be expressed as lists.
     Returns:
         null
     
     */

    if (is_array(data)){
        for (var i=0; i< data.length; i++){
            if (is_array(data[i])){
                data[i] = to_array(data[i], 'd')
            }
        }
        data = to_array(data, 'o')
    }        
    get_context().dataManager.appendItem(path, data)
}

function flush_data() {
    /*
     Flush all data files immediately.
     
     Args:
        null
     Returns:
         null
     */
    get_context().dataManager.flush()
}

function set_attribute(path, name, value, unsigned) {
    /*
     Set an attribute to a group or dataset.
     
     Args:
        path(str): Path to dataset relative to the current persistence context root.
        name(str): name of the atttribute
        value(Object): the attribute value
        unsigned(bool, optional):  if applies, indicate if  value is unsigned.
     Returns:
         null
     */
    if (!is_defined(unsigned))
        unsigned = false;
    get_context().dataManager.setAttribute(path, name, value, unsigned)
}

function log(log, data_file){
    /*
    Writes a log to the automatic data saving context - only if there is an ongoing scan or 
       script execution.        

    Args:        
         log(str): Log string.
         data_file(bool, optional): if true logs to the data file, in addiction to the system logger
                                    If None(default) appends to data file only if it has been created.

    Returns:
        None
    */  
    if (!is_defined(data_file))
        data_file = get_exec_pars().open
    get_context().scriptingLog(String(log))
    if (data_file){
       try{
            get_context().dataManager.appendLog(String(log))
        } catch(err){    
            //Do not generate exception if cannot write to data file
        }
    }     
}

function set_exec_pars(args){
    /*
       Configures the script execution parameters, overriding the system configuration.
    
    Args: 
      args(optional arguments):
        name(str): value of the {name} tag. Default is the running script name.
        type(str): value of the {type} tag. Default is empty.
                             This field can be used to store data in  sub-folders of standard location.
        path(str):  If defined provides the full path name for data output root (overriding config))
                             The tag {data} can be used to enter a path relative to the standard data folder.
        layout(str): Overrides default data layout.
        provider(str): Overrides default data provider.
        depth_dim(int): dimension of 2d-matrixes in 3d datasets.
        save(bool): Overrides the configuration option to auto save scan data.
        flush(bool): Overrides the configuration option to flush file on each record.
        keep(bool): Overrides the configuration option to release scan records.
                                    If false disable accumulation of scan records to scan result.
        preserve(bool): Overrides the configuration option to preserve device types.
                                  If false all values are converted to double.
        compression(obj): True for enabling default compression, int for specifying deflation level.                                    
                                    Device or list of devices for specifying devices to be compressed.
        shuffle(obj): True for enabling shuffling before compression. 
                                Device or list of devices for specifying devices to be shuffled.
        contiguous(obj): True for setting contiguous datasets for all devices.
                                   Device or list of devices for specifying device datasets to be contiguous.
        open(bool): If true opens data output root (instead of only doing in the first data access call)
                              If false closes output root, if open.
        reset(bool): If true reset the scan counter - the {count} tag and set the timestamp to now.
        group(str): Overrides default layout group name for scans
        tag(str): Overrides default tag for scan names (affecting group or dataset name, according to layout)
        then, then_success, then_exception(str): Sets statement to be executed on the completion of current. 
        defaults(bool): If true restore the original execution parameters.

        Graphical preferences can also be set. Keys are equal to lowercase of Preference enum:
        "plot_disabled", "plot_layout", "table_disabled", "enabled_plots", "plot_types", "print_scan", "auto_range", 
        "manual_range","domain_axis", "status".
        See set_preference for more information.

        Shortcut entries: "line_plots": list of devices with enforced line plots.
    */
    get_context().setExecutionPars(args)
}
    
function get_exec_pars(){
    /*
    Returns script execution parameters.

    Returns:
        ExecutionParameters object. Fields: 
            name (str): execution name - {name} tag.
            type (str): execution type - {type} tag.
            path (str): output data root.
            open (bool): true if the output data root has been opened.
            layout (str): data output layout. If null then using the configuration.
            save (bool): auto save scan data option. 
            flush (bool): flush file on each record.
            index (int): current scan index.
            group (str): data group currently used for scan data storage. 
                         if no ongoing scan return "/" if within a script, or else null if a console command.
            scanPath (str): dataset or group corresponding to current scan.
            scan (Scan): reference to current scan, if any
            source (CommandSource): return the source of the script or command.
            args (obj): return the arguments for the script.
            background (bool): return False if executing in main interpreter thread .
    */
    return get_context().getExecutionPars()    
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// Epics Channels access
///////////////////////////////////////////////////////////////////////////////////////////////////

function caget(name, type, size, meta) {
    /*
     Reads an Epics PV.
     
    Args:
        name(str): PV name
        type(str, optional): type of PV. By default gets the PV standard field type.
            Scalar values: 'b', 'i', 'l', 'd', 's'.
            Array: values: '[b', '[i,', '[l', '[d', '[s'.
        size (int, optional): for arrays, number of elements to be read. Default read all.
        meta (bool, optional): if true gets channel value and metadata (timestamp, severity).

    Returns:
        PV value if meta is false, otherwise a dictionary containing PV value and metadata
     
     */
    if (!is_defined(type))
        type = null;
    if (!is_defined(size))
        size = null;
    if (!is_defined(meta))
        meta = false;
    if (meta)
        return Epics.getMeta(name, Epics.getChannelType(type), size)
    return Epics.get(name, Epics.getChannelType(type), size)
}

function cawait(name, value, timeout, comparator, type, size) {
    /*
     Wait for a PV to have a given value.
     
     Args:
        name(str): PV name
        value (obj): value to compare to
        timeout(float, optional): time in seconds to wait. If null, waits forever.
        comparator(java.util.Comparator or float, optional): if None waits for equality. 
            If a numeric value is provided, waits for channel to be in range.
        type(str, optional): type of PV. By default gets the PV standard field type.
        Scalar values: 'b', 'i', 'l', 'd', 's'.
        Array: values: '[b', '[i,', '[l', '[d', '[s'.
        size (int, optional): for arrays, number of elements to be read. Default read all.
     
     Returns:
         None
     */
    if (!is_defined(type))   type = null;
    if (!is_defined(size))   size = null;
    if (!is_defined(comparator)) comparator = null;
    if (is_defined(timeout))
        timeout = timeout * 1000
    else
        timeout = null
    return Epics.waitValue(name, value, comparator, timeout, Epics.getChannelType(type), size)
}

function caput(name, value, timeout) {
    /*
     Writes to an Epics PV.
     
     Args:
        name(str): PV name
        value(scalar, string or array): new PV value.
        timeout(int, optional): timeout in seconds to the write. If null waits forever to completion.                    

     Returns:
         None
     */
    if (is_defined(timeout))
        timeout = timeout * 1000
    else
        timeout = null
    return Epics.put(name, value, timeout)
}

function caputq(name, value) {
    /*
     Writes to an Epics PV and does not wait.
     
     Args:
        name(str): PV name
        value(scalar, string or array): new PV value.
     
     Returns:
         None
     */
    return Epics.putq(name, value)
}


function create_channel(name, type, size) {
    if (!is_defined(type))
        type = null;
    if (!is_defined(size))
        size = null;
    return Epics.newChannel(name, Epics.getChannelType(type), size)
}


function create_channel_device(channelName, type, size, deviceName){
    if (!is_defined(type))
        type = null;
    if (!is_defined(size))
        size = null;
    if (!is_defined(deviceName))
        deviceName = null;
    dev = Epics.newChannelDevice(deviceName, channelName,Epics.getChannelType(type))
    if (get_context().isSimulation()){
        dev.setSimulated()
    }
    dev.initialize()
    if (size != null){
        dev.setSize(size)
    }
    return dev
}

///////////////////////////////////////////////////////////////////////////////////////////////////
// Concurrent execution 
///////////////////////////////////////////////////////////////////////////////////////////////////
function _getCallable(func, args) {
    var _parent_thread = java.lang.Thread.currentThread()
    var callable = new java.util.concurrent.Callable() {
        call: function () {      
            try {
                get_context().startedChildThread(_parent_thread)
                return func.apply( this, args );
            }
            finally {
                get_context().finishedChildThread(_parent_thread)
            }     
            
        }
    }
    return callable
}

function fork() {
    /*
     Start execution of methods in parallel. 
     
     Args:
        *functions(functions references)
     
     Returns:
        List of callable objects
     */
    var callables = []
    for( var i =0; i<arguments.length; i++){
        var m = arguments[i]
        if (get_rank(m)>0){
            callables.push(_getCallable(m[0], m[1]))
        }
        else{
            callables.push(_getCallable(m))
        }
        
    }
    return Threading.fork(callables)
}

function join(futures) {
    /*
     Wait parallel execution of methods.
     
     Args:
        callables(list of Callables) : as returned from fork
     
     Returns:
        None
     */
    try{
        return Threading.join(futures)
    } catch(err){    
        throw  err.getCause()
    }
}

function parallelize() {
    /*
     Equivalent to fork + join
     
     Args:
        *methods(method references)
     
     Returns:
        None
     */
    futures = fork.apply(this, arguments)
    return join(futures)
}

///////////////////////////////////////////////////////////////////////////////////////////////////
// Script evaluation and Background task control.
///////////////////////////////////////////////////////////////////////////////////////////////////
function run(script_name, args) {
    /*
     Run script: can be absolute path, relative, or short name to be search in the path.
     Args:
        args(Dict ot Array): gobal variables set to the script(if dict), or argv varialble (if array).
     
     Returns:
        The script return value
    */
    var script = get_context().scriptManager.library.resolveFile(script_name)
    var file = script!=null ? new File(script) : null
    if ((file == null) ||  ( ! file.exists())) throw "Invalid script: " + script_name
    get_context().startScriptExecution(args)

    if (is_defined(args) &&  (args!=null)){
        if (is_array(args)){
            argv = args
        } else {
            for (var key in args) {
                eval(key+"="+args[key])
            }
        }
    }
    //eval(new String(Files.readAllBytes(file.toPath())))
    //get_context().scriptManager.interpreter.evalFile(script);
    load(script)
}


function _abort() {
     get_context().abort()
}

function abort() {
    /*
    Abort the execution of ongoing task. It can be called from the script to quit.

    Args:                 
        None

    Returns:
        None
    */
    //Cannot be on script execution thread
    fork(_abort)  
}

function start_task(script, delay, interval){
    /*
    Start a background task

    Args:        
         script(str): Name of the script implementing the task
         delay(float, optional): time in seconds for the first execution. 
                Default starts immediately.
         interval(float, optional): time in seconds for between execution. 
                If negative (default), single-execution.

    Returns:
        None
    */      
    if (!is_defined(delay))   delay = 0.0;
    if (!is_defined(interval))   interval = -1;
    var delay_ms = delay_ms * 1000
    var interval_ms = (interval>=0) ? interval_ms * 1000 : interval
    get_context().taskManager.create(script, delay_ms, interval_ms)
    get_context().taskManager.start(script)
}
    
function stop_task(script, force){
    /*
    Stop a background task

    Args:        
         script(str): Name of the script implementing the task
         force(boolean, optional): interrupt current execution, if running

    Returns:
        None
    */      
    if (!is_defined(force))   force = false;
    get_context().taskManager.remove(script, force)
}

function set_return(value){
    /*
    Sets the script return value. This value is returned by the "run" function.

    Args:
        value(Object): script return value.

    Returns:
        None
    */  
    if (is_interpreter_thread()){
        _=value       
    }
    __THREAD_EXEC_RESULT__.put(java.lang.Thread.currentThread(),value)         //Used when running file
    return value    //Used when parsing file  
}

function is_interpreter_thread(){
    return java.lang.Thread.currentThread().name == "Interpreter Thread" 
}

///////////////////////////////////////////////////////////////////////////////////////////////////
// Versioning tools
///////////////////////////////////////////////////////////////////////////////////////////////////
function commit(message, force){
    /*
    Commit the changes to the repository. If manual commit is not configured then there is no need to call this function: commits are made as needed.

    Args:
        message(str): commit message
        force(bool, optional): if False, raises exception if no change detected in repo
    Returns:
        None
    */
    if (!is_defined(force))   force = false;   
    get_context().commit(message, force)
}

function diff(){
    /*
    Return list of changes in the repository

    Args:
        None

    Returns:
        None
    */   
    return get_context().diff()
}

function checkout_tag(tag){
    /*     
    Checkout a tag name.

    Args:
        tag(str): tag name.

    Returns:
        None
    */
    get_context().checkoutTag(tag)
}

function checkout_branch(tag){
    /*
    Checkout a local branch name.

    Args:
        tag(str): branch name.

    Returns:
        None
    */
    get_context().checkoutLocalBranch(tag)
}

function pull_repository(){
    /*
    Push to remote repository.

    Args:
        all_branches(boolean, optional): all branches or just current.
        force(boolean, optional): force flag.

    Returns:
        None
    */
    get_context().pullFromUpstream()
}

function push_repository(all_branches, force){
    /*
    Push to remote repository.

    Args:
        all_branches(boolean, optional): all branches or just current.
        force(boolean, optional): force flag.

    Returns:
        None
    */
    if (!is_defined(all_branches))    all_branches = true;   
    if (!is_defined(force))    force = false;
    
    get_context().pushToUpstream(all_branches, force)
}

function cleanup_repository(){
    /*
    Performs a repository cleanup.

    Args:
        None

    Returns:
        None
    */    
    get_context().cleanupRepository()
}

///////////////////////////////////////////////////////////////////////////////////////////////////
// Device Pool & Imaging setup
///////////////////////////////////////////////////////////////////////////////////////////////////
function get_device(device_name){
    /*
    Returns a configured device (or imaging source) by its name. 

    Args:
        device_name(str): name of the device.

    Returns:
        device
    */    
    return get_context().devicePool.getByName(device_name)
}

function add_device(device, force){
    /*
    Add  a device (or imaging source) to the device pool.

    Args:
        device(Device or Source): device object.
        force(boolean, optional): if true then dispose existing device with same name. 
                    Otherwise will fail in case of name clash.

    Returns:
        True if device was added, false if was already in the pool, or exception in case of name clash.
    */
    if (!is_defined(force))    force = false;
    if (get_context().devicePool.contains(device)){
        return false
    }
    if (force){
        dev = get_context().devicePool.getByName(device.getName())
        if (dev!=null) 
            remove_device(dev)
    }
    return get_context().devicePool.addDevice(device)
}

function remove_device(device){
    /*
    Remove a device (or imaging source) from the device pool.

    Args:
        device(Device or Source): device object.

    Returns:
        bool: true if device was removed.

    */
    return get_context().devicePool.removeDevice(device)
}

function set_device_alias(device, alias){
    /*
    Set a device alias to be used in scans (datasets and plots).

    Args:
        device(Device): device object.
        alias(str): replace device name in scans.

    Returns:
        None
    */
    get_context().dataManager.setAlias(device, alias)
}    

function stop(){
    /*
    Stop all devices implementing the Stoppable interface.

    Args:                 
        None

    Returns:
        None
    */
    get_context().stopAll()
}
    
function update(){
    /*
    Update all devices.

    Args:                 
        None

    Returns:
        None
    */
    get_context().updateAll()
}

function reinit(dev){
    /*
    Re-initialize devices.

    Args:                 
        dev(Device, optional): the device to be re-initialized. 
                               If  null re-initialize all devices not yet initialized.

    Returns:
        List with devices not initialized.
    */
    if (!is_defined(dev))    dev = null;
    return to_array(get_context().reinit())
}


function create_device(url, parent){
    /*
    Create a device form a definition string(see InlineDevice)

    Args:                 
        url(str): the device definition string
        parent(bool, optional): parent device

    Returns:
        The created device
    */
    if (!is_defined(parent))    parent = null;
    return InlineDevice.create(url, parent)
}

function create_averager(dev, count, interval, name, monitored){
    /*
    Creates and initializes and averager for dev.

    Args:                 
        dev(Device): the source device
        count(int): number of samples
        interval(float, optional): sampling interval in seconds. 
                                   If less than zero, sampling is made on data change event.
    name(str, optional): sets the name of the device (default is: <dev name> averager)    
    monitored (bool, optional): if true then averager processes asynchronously.

    Returns:
        Averager device
    */
    if (!is_defined(interval))    interval = 0.0;
    if (!is_defined(name))    name = null;
    if (!is_defined(monitored))    monitored = false;
    dev = string_to_obj(dev)
    if (dev instanceof ReadableArray) {
        var averager = (name == null) ? new ArrayAverager(dev, count, interval*1000) : new ArrayAverager(name, dev, count, interval*1000)
    }else{
        var averager = (name == null) ? new Averager(dev, count, interval*1000) : new Averager(name, dev, count, interval*1000)
    }
    if (monitored){
        averager.setMonitored(true)
    }
    averager.initialize()
    return averager
}

///////////////////////////////////////////////////////////////////////////////////////////////////
//Mathematical functions
///////////////////////////////////////////////////////////////////////////////////////////////////

function mean(data){
    /*
    Calculate the mean of a sequence.

    Args:
        data(list, tuple, array ...): subscriptable object containing numbers

    Returns:
        Mean of the elements in the object.

    */
    if (!is_array(data)){
        data = to_array(data)
    }
    return data.reduce(function(sum, value){return sum + value;}, 0) / data.length;
}

function variance(data){
    /*
    Calculate the variance of a sequence.

    Args:
        data(list, tuple, array ...): subscriptable object containing numbers

    Returns:
        Variance of the elements in the object.

    */
    var c = mean(data)
    return mean(data.map(function(value){return Math.pow((value - c),2);}));
}

function stdev(data){
    /*
    Calculate the standard deviation of a sequence.

    Args:
        data(list, tuple, array ...): subscriptable object containing numbers

    Returns:
        Standard deviation of the elements in the object.

    */
    return Math.sqrt(variance(data))
}

function poly(val, coefs){
    /*
    Evaluates a polinomial: (coefs[0] + coefs[1]*val + coefs[2]*val^2...

    Args:
        val(float): value
        coefs (list of loats): polinomial coefficients 
    Returns:
        Evaluated function for val 

    */
    var r = 0
    var p = 0
    for (c in coefs){
        r = r + coefs[c] * Math.pow(val, p)
        p = p + 1
    }
    return r
}

function histogram(data, range_min, range_max, bin){
    /*
    Creates histogram on data.

    Args:
        data (tuple, array, ArrayList or Array): input  data can be multi-dimensional or nested.
        range_min (int, optional): minimum histogram value. Default is floor(min(data))
        range_max (int, optional): maximul histogram value. Default is ceil(max(data))
        bin(int or float, optional): if int means number of bins. If float means bin size. Default = 1.0.
    Returns:
        tuple: (ydata, xdata)

    */
    flat = flatten(data)
    if (!is_defined(range_min))    range_max = null
    if (!is_defined(range_max))    range_max = null
    if (!is_defined(bin))    bin = 1.0    
    if (range_min == null) range_min = Math.floor(Math.min.apply(null,flat))
    if (range_max == null) range_max = Math.ceil(Math.max.apply(null,flat))
    
    if (is_float(bin)){    	
        bin_size = bin
        n_bin =  Math.ceil((range_max - range_min)/bin_size)
    }
    else{    	
        n_bin = bin
        bin_size = (range_max - range_min)/bin
    }   
    var result = []; var size=n_bin; while(size--) result.push(0)        
    for  (var d in flat){
        b = Math.floor( (flat[d] - range_min) / bin_size)
        if ((b >=0) && (b < n_bin)){
          result[b] = result[b] + 1
        }
    }
    var result_x = []; var size=result.length; var p=range_min; while(size--) {result_x.push(p); p+=bin_size }    
    return [result,result_x]
}

function cmp(a, b){
    if (a>b) return 1
    if (a<b) return -1
    return 0
}

function hypot(a,b){
    if (!is_defined(a))    a = 0
    if (!is_defined(b))    b = 0

    return Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2))
}

function cross(a, b, o) {
   return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
}

/**
 * From https://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain#JavaScript
 */
function convex_hull(points, fx, fy) {
    if (!is_defined(points))    points = null
    if (!is_defined(fx))    fx = null
    if (!is_defined(fy))    fy = null
    var is_point_list = points != null    
    if (!is_point_list){
        points=[]
        for (var i=0; i<fx.length; i++){
            if((fx[i] != null) && (fy[i] != null)) points.push([fx[i], fy[i]])            
        }
    }    	
   points.sort(function(a, b) {
      return a[0] == b[0] ? a[1] - b[1] : a[0] - b[0];
   });

   var lower = [];
   for (var i = 0; i < points.length; i++) {
      while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], points[i]) <= 0) {
         lower.pop();
      }
      lower.push(points[i]);
   }

   var upper = [];
   for (var i = points.length - 1; i >= 0; i--) {
      while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], points[i]) <= 0) {
         upper.pop();
      }
      upper.push(points[i]);
   }

   upper.pop();
   lower.pop();
   var hull = lower.concat(upper);
    if (!is_point_list){
        var px=[]
        var py=[]
        for (var i=0; i<hull.length; i++){
            px.push(hull[i][0])
            py.push(hull[i][1])
        }
        return [px,py]
    }   
    return hull
}


///////////////////////////////////////////////////////////////////////////////////////////////////
// Utilities
///////////////////////////////////////////////////////////////////////////////////////////////////

function get_setting(name){
    /*
    Get a persisted script setting value.

    Args:
        name (str): name of the setting.
    Returns:
        String with setting value or None if setting is undefined.
        If name is None then returns map with all settings.
    */
    if (!is_defined(name))    name = null
    if (name == null){
        return get_context().getSettings()
    } else {
        return get_context().getSetting(name)
    }
}

function set_setting(name, value){
    /*
    Set a persisted script setting value.

    Args:
        name (str): name of the setting.
        value (obj): value for the setting, converted to string (if None then remove the setting).
    Returns:
        None.
    */
    if (!is_defined(value))    value = null
    get_context().setSetting(name, value)
}

function exec_cmd(cmd){
    /*
    Executes a shell command. If errors happens raises an exception.

    Args:
        cmd (str): command process input.
    Returns:
        Output of command process.
    */
    proc = java.lang.Runtime.getRuntime().exec(cmd, null);
    proc.waitFor()

    bytes = proc.getErrorStream().available()
    if (bytes>0){
        var ByteArray = Java.type("byte[]");
        var arr = new ByteArray(bytes);
        proc.getErrorStream().read(arr, 0, bytes)
        throw new java.lang.String(arr)
    }

    bytes = proc.getInputStream().available()    
    if (bytes>0){
        var ByteArray = Java.type("byte[]");
        var arr = new ByteArray(bytes);
        proc.getInputStream().read(arr, 0, bytes)
        return new java.lang.String(arr)
    }
    return null
}

function exec_cpython(script_name, args, python_name){
    /*Executes an external cpython process.

    Args:
        script_name (str): name of the script (can be absolute or relative to script folder).
        args(list, optional): arguments to python process (or parameters to method, if not null)
        python_name (str, optional): name of executable
    Returns:
        Return of python process.
    */
    if (!is_defined(args))    args = []  
    if (!is_defined(python_name))    python_name = "python"

    if (!script_name.toLowerCase().endsWith(".py")){
            script_name += ".py"
    }
    script = get_context().scriptManager.library.resolveFile(script_name)
    if (script == null){
        script= os.path.abspath(script_name)
    }
    c = python_name + " " + script + " "
    if ( args ){
        for (var arg in args){
            c = c + args[arg] + " "    
        }
    }
    return exec_cmd(c)
}

function string_to_obj(o) {
    if (typeof o === 'string') {
        if (o.contains("://")){
            return new InlineDevice(o)   
        }            
        return eval(o)
    } else if (o instanceof Array) {
        ret = []
        for (var i = 0; i < o.length; i++)
            ret.push(string_to_obj(o[i]));
        return ret
    }
    return o
}

function bsget(channel, modulo, offset, timeout){
    /* Reads an values a bsread stream, using the default provider.

    Args:
        channel(str or  list of str): channel name(s)
        module(int, optional): stream modulo
        offset(int, optional): stream offset
        timeout(float, optional): stream timeout in secs
    Returns:
        BS value or list of  values
    
    */
    if (!is_defined(modulo))    modulo = 1
    if (!is_defined(offset))    offset = 0
    if (!is_defined(timeout))    timeout=5.0
    var channels = (typeof channel === 'string')? [channel]: channel
    var ret = Stream.readChannels(channels, modulo, offset, timeout*1000)
    if (typeof channel === 'string') {
        return ret[0]
    }
    return ret
}

function flatten(arr) {
    var ret = [];
    if (is_java_list(arr)){
        arr = to_array(arr)
    } else if (is_java_array(arr)) {
        arr =  Java.from(arr);
    }
    
    for(var i = 0; i < arr.length; i++) {
        if((is_java_list(arr[i])) || (is_java_array(arr[i])) || (is_array(arr[i]))) {
            ret = ret.concat(flatten(arr[i]));
        } else {
            ret.push(arr[i]);
        }
    }
    return ret;
}
function range(start, stop, step, add_last) {
    if (!is_defined(add_last))    add_last = false 
    var ret = []
    var cur = start;
    if (((start>stop) && (step>0)) ||((start<stop) && (step<0))){
        throw "Invalid range parameters"
    }
    while (((step >= 0.0) && (cur < stop)) || ((step < 0.0) && (cur > start))){        
        ret.push(cur)
        cur += step;
    }
    if (add_last)  ret.push(stop)
    return ret;
}

function inject(){
    /*
    Restore initial globals: re-inject devices and startup variables to the interpreter.

    Args:
        None

    Returns:
        None

    */  
    get_context().injectVars()
}

function notify(subject, text, attachments, to){
    /*Send email message.

    Args:
        subject(str): Message subject.
        text(str): Message body.
        attachments(list of str, optional): list of files to be attached (expansion tokens are allowed).
        to (list ofd str, optional): recipients. If None uses the recipients defined in mail.properties.
    Returns:
        None

    */  
    if (!is_defined(attachments))    attachments = null
    if (!is_defined(to))    to = null
    get_context().notify(subject, text, attachments, to)
}

function sleep(seconds){
    java.lang.Thread.sleep(seconds * 1000);
}


function sort_indexes(arr, decreasing) {
  arr = to_array(arr)
  var sort = arr.slice(0)
  for (var i = 0; i < sort.length; i++) {
    sort[i] = [sort[i], i];
  }
  sort.sort(function(left, right) {
    return left[0] < right[0] ? -1 : 1;
  });
  var sort_indices = [];
  for (var i = 0; i < sort.length; i++) {
    sort_indices.push(sort[i][1]);
    sort[i] = sort[i][0];
  }
  if (decreasing){
      sort_indices.reverse()  
  }
  return sort_indices;
}

function _getBuiltinFunctionNames(){
    ret = []
    for (var obj in this) { 	
        if (typeof this[obj] == "function" && 
            this.hasOwnProperty(obj) &&
                !(/[A-Z]/.test(obj)) &&         //TODO: This checking is not good, it is to filter all classes names seen as function
                !(obj.startsWith("_"))
            ) {
                var str = this[obj].toString()
                if (str.contains("\*\/") && str.contains("\/\*") &&  str.contains("{")){
                    ret.push(obj.toString())
                }
            } 
    }
    return ret
}

function _getFunctionDoc(f){
	if (typeof f == "function"){
		f = f.name
	}	
    var str = this[f].toString()
    
    str = str.trim()
    if (str.startsWith("function ")){
        str = str.substring(9).trim()
    }
    if (str.contains("\*\/")){    	
        str = str.substring(0, str.indexOf("\*\/")+2)        
        if (str.contains("{")){
            var index = str.indexOf("{")
            return  (str.substring(0, index) + str.substring(index+1)).trim()
        }        
    }        
    return null      
}
    
function help(object){
    /* Print help message for function or object (if available).

    Args:
        object (any, optional): function or object to get help. 
                    If null prints a list of the builtin functions.

    Returns:
        None
    
    */
    if (!is_defined(object)){
        print ("Built-in functions:")
        var names = _getBuiltinFunctionNames()
        for (var f in names){
            print ("\t" + names[f])
        }
    } else {        
        //if (typeof object == "function"){
        //    print this[f.name].toString() 
        //} else {
            print(object)
        //}
    }
}

///////////////////////////////////////////////////////////////////////////////////////////////////
//UI interaction
///////////////////////////////////////////////////////////////////////////////////////////////////

function set_status(status){
    /*
    Set the application status.

    Args:
        status(str): new status.

    Returns:
        None
    */
    set_preference(ViewPreference.STATUS, status)
}

function set_preference(preference, value){
    /*
    Hints to graphical layer:    

    Args:
        preference(Preference): Enum of preference types:
            PLOT_DISABLED: enable/disable scan plot (True/False)
            PLOT_LAYOUT: "Horizontal", "Vertical" or "Grid"
            TABLE_DISABLED: enable/disable scan table (True/False)
            ENABLED_PLOTS: select Readables to be plotted (list of Readable or String (names))
            PLOT_TYPES: Dictionary or (Readable or String):(String or int) pairs
                where the key is a plot name and the value is the desired plot type
            PRINT_SCAN: Print scan records to console
            AUTO_RANGE: Automatic range scan plots x-axis
            MANUAL_RANGE: Manually set scan plots x-axis
            DOMAIN_AXIS: Set the domain axis source: "Time", "Index", or a readable name.
                Default(None): first positioner
            STATUS: set application status
        value(object): preference value

    Returns:
        None
    */  
    if (get_rank(value)>0){
        value = to_array(value, 'o') 
    }
    get_context().setPreference(preference, value)
}

function get_string(msg, default_value, alternatives, password){
    /*
    Reads a string from UI
    Args:
        msg(str): display message.
        default_value(str, optional): value displayed when window is shown.
        alternatives(list of str, optional): if provided presents a combo box instead of an editing field.
        password(boolean, optional): if True hides entered characters.

    Returns:
        String entered of null if canceled
    */
    if (!is_defined(default_value))    default_value =  null;
    if (!is_defined(alternatives))    alternatives = null;
    if (!is_defined(password))    password = false;
    if (password){
        return get_context().getPassword(msg, null)
    }
    return get_context().getString(msg, (default_value==null) ? null: default_value.toString(), alternatives)
}

function get_option(msg, type ){
    /*
    Gets an option from UI        
    Args:
        msg(str): display message.
        type(str, optional): 'YesNo','YesNoCancel' or 'OkCancel'

    Returns:
        'Yes', 'No', 'Cancel'

    */
    if (!is_defined(type))    type = "YesNoCancel";
    return get_context().getOption(msg, type)
}

function show_message(msg, title, blocking){
    /*
    Pops a blocking message to UI
    
    Args:
        msg(str): display message.    
        title(str, optional): dialog title
    */
    if (!is_defined(title))    title = null;
    if (!is_defined(blocking))    title = true;
    get_context().showMessage(msg, title, blocking)
}

function show_panel(device, title){
    /*
    Show, if exists, the panel relative to this device.
    
    Args:
        device(Device or str or BufferedImage): device
        title only apply to BufferedImage objects. For devices the title is the device name.
    */
    if (!is_defined(title))    title = null;
    if (device.class == BufferedImage.class){
        device = new DirectSource(title, device)
        device.initialize()
    }
    if (typeof device == 'string'){
        device = get_device(device)
    }
    return get_context().showPanel(device)
}

