require.config({
    baseUrl : '.',
    paths : {
        jquery : '3rdparty/jquery/jquery-1.11.2.min',
        'jquery-ui' : '3rdparty/jquery/ui',
        knockout : '3rdparty/knockout/knockout-3.2.0',
        kojqui : '3rdparty/knockout-jqueryui',
        sprintf : '3rdparty/sprintf/sprintf.min',
        leaflet : '3rdparty/leaflet-0.7.3/leaflet',
        text : '3rdparty/require/text',
        flot : '3rdparty/flot/jquery.flot',
        flotresize : '3rdparty/flot/jquery.flot.resize',
        flottime : '3rdparty/flot/jquery.flot.time',
        fgcommand : 'lib/fgcommand',
        props : 'lib/props2',
        sammy: '3rdparty/sammy-latest.min',
        aircraft: '../aircraft-dir',
        pagedown: '3rdparty/pagedown'
    }
});

require([
        'knockout', 'jquery','sammy', 'fgcommand', 'themeswitch', 'kojqui/button', 'kojqui/buttonset', 'kojqui/selectmenu', 'jquery-ui/sortable', 'flot', 'leaflet'
], function(ko, jquery, Sammy, fgcommand ) {

    function KnockProps(aliases) {

        var self = this;

        self.initWebsocket = function() {
            self.ws = new WebSocket('ws://' + location.host + '/PropertyListener');

            self.ws.onclose = function(ev) {
                var msg = 'Lost connection to FlightGear. Should I try to reconnect?';
                if (confirm(msg)) {
                    // try reconnect
                    self.initWebsocket();
                } else {
                    throw new Error(msg);
                }
            }

            self.ws.onerror = function(ev) {
                var msg = 'Error communicating with FlightGear. Please reload this page and/or restart FlightGear.';
                alert(msg);
                throw new Error(msg);
            }

            self.ws.onmessage = function(ev) {
                try {
                    self.fire(JSON.parse(ev.data));
                } catch (e) {
                }
            };

            self.openCache = [];
            self.ws.onopen = function(ev) {
                // send subscriptions when the socket is open
                var c = self.openCache;
                delete self.openCache;
                c.forEach(function(e) {
                    self.addListener(e.prop, e.koObservable);
                });
                for ( var p in self.listeners) {
                    self.addListener(p, self.listeners[p]);
                }
            }

        }

        self.initWebsocket();

        self.fire = function(json) {
            var value = json.value;
            var listeners = self.listeners[json.path] || [];
            listeners.forEach(function(koObservable) {
                koObservable(value)
            });
            koObservable(json.value);
        }

        function resolvePropertyPath(self, pathOrAlias) {
            if (pathOrAlias in self.aliases)
                return self.aliases[pathOrAlias];
            if (pathOrAlias.charAt(0) == '/')
                return pathOrAlias;
            return null;
        }

        self.listeners = {}

        self.removeListener = function(pathOrAlias, koObservable) {
            var path = resolvePropertyPath(self, pathOrAlias);
            if (path == null) {
                console.log("can't remove listener for " + pathOrAlias + ": unknown alias or invalid path.");
                return self;
            }

            var listeners = self.listeners[path] || [];
            var idx = listeners.indexOf(koObservable);
            if (idx == -1) {
                console.log("can't remove listener for " + path + ": not a listener.");
                return self;
            }

            listeners.splice(idx, 1);

            if (0 == listeners.length) {
                self.ws.send(JSON.stringify({
                    command : 'removeListener',
                    node : path
                }));
            }

            return self;
        }

        self.addListener = function(alias, koObservable) {
            if (self.openCache) {
                // socket not yet open, just cache the request
                self.openCache.push({
                    "prop" : alias,
                    "koObservable" : koObservable
                });
                return self;
            }

            var path = resolvePropertyPath(self, alias);
            if (path == null) {
                console.log("can't listen to " + alias + ": unknown alias or invalid path.");
                return self;
            }

            var listeners = self.listeners[path] = (self.listeners[path] || []);
            if (listeners.indexOf(koObservable) != -1) {
                console.log("won't listen to " + path + ": duplicate.");
                return self;
            }

            koObservable.fgPropertyPath = path;
            koObservable.fgBaseDispose = koObservable.dispose;
            koObservable.dispose = function() {
                if( this.fgPropertyPath ) {
                    self.removeListener( this.fgPropertyPath, this );
                }
                this.fgBaseDispose.call(this);
            }
            listeners.push(koObservable);
            koObservable.fgSetPropertyValue = function(value) {
                self.setPropertyValue( this.fgPropertyPath, value );
            }

            if (1 == listeners.length) {
                self.ws.send(JSON.stringify({
                    command : 'addListener',
                    node : path
                }));
            }
            self.ws.send(JSON.stringify({
                command : 'get',
                node : path
            }));

            return self;
        }

        self.aliases = aliases || {};
        self.setAliases = function(arg) {
            arg.forEach(function(a) {
                self.aliases[a[0]] = a[1];
            });
        }

        self.props = {};

        self.get = function(target, prop) {
            if (self.props[prop]) {
                return self.props[prop];
            }

            return (self.props[prop] = self.observedProperty( target, prop )); 
        }
        
        self.observedProperty = function( target, prop ) {
            var reply = ko.pureComputed({
                read: target,
                write: function(newValue) {
                    if( newValue == target() )
                        return;
                    target(newValue);
                    target.notifySubscribers(newValue);
                }
            });
            self.addListener(prop, reply);
            return reply;
        }

        self.write = function(prop, value) {
            var path = this.aliases[prop] || "";
            if (path.length == 0) {
                console.log("can't write " + prop + ": unknown alias.");
                return;
            }

            self.setPropertyValue(path,value);
        }

        self.setPropertyValue = function(path, value) {
            this.ws.send(JSON.stringify({
                command : 'set',
                node : path,
                value : value
            }));
        }

        self.propsToObject = function(prop, map, result) {
            result = result || {}
            prop.children.forEach(function(prop) {
                var target = map[prop.name] || null;
                if (target) {
                    if (typeof (result[target]) === 'function') {
                        result[target](prop.value);
                    } else {
                        result[target] = prop.value;
                    }
                }
            });
            return result;
        }
    }

    ko.extenders.fgprop = function(target, prop) {
        return ko.utils.knockprops.get(target, prop);
    };
    
    ko.extenders.observedProperty = function(target,prop) {
        return ko.utils.knockprops.observedProperty(target,prop);
    };

    ko.extenders.fgPropertyGetSet = function(target,option) {

        fgCommand.getPropertyValue(option, function(value) {
          target(value);
        }, self);

        var p = ko.pureComputed({
            read : target,
            write : function(newValue) {
                if (newValue == target())
                    return;
                target(newValue);
                target.notifySubscribers(newValue);
                fgCommand.setPropertyValue(option, newValue );
            }
        });
        return p;
    }


    ko.utils.knockprops = new KnockProps();

    ko.utils.knockprops.setAliases([
            // time
            [
                    "gmt", "/sim/time/gmt"
            ], [
                    "local-offset", "/sim/time/local-offset"
            ],

            // flight
            [
                    "pitch", "/orientation/pitch-deg"
            ], [
                    "roll", "/orientation/roll-deg"
            ], [
                    "heading", "/orientation/heading-magnetic-deg"
            ], [
                    "true-heading", "/orientation/heading-deg"
            ], [
                    "altitude", "/position/altitude-ft"
            ], [
                    "latitude", "/position/latitude-deg"
            ], [
                    "longitude", "/position/longitude-deg"
            ], [
                    "airspeed", "/velocities/airspeed-kt"
            ], [
                    "groundspeed", "/velocities/groundspeed-kt"
            ], [
                    "slip", "/instrumentation/slip-skid-ball/indicated-slip-skid"
            ], [
                    "cg", "/fdm/jsbsim/inertia/cg-x-in"
            ], [
                    "weight", "/fdm/jsbsim/inertia/weight-lbs"
            ],
            // radio settings
            [
                    "com1stn", "/instrumentation/comm/station-name"
            ], [
                    "com1use", "/instrumentation/comm/frequencies/selected-mhz"
            ], [
                    "com1sby", "/instrumentation/comm/frequencies/standby-mhz"
            ], [
                    "com1stn", "/instrumentation/comm/station-name"
            ], [
                    "com2stn", "/instrumentation/comm[1]/station-name"
            ], [
                    "com2use", "/instrumentation/comm[1]/frequencies/selected-mhz"
            ], [
                    "com2sby", "/instrumentation/comm[1]/frequencies/standby-mhz"
            ], [
                    "com2stn", "/instrumentation/comm[1]/station-name"
            ], [
                    "nav1use", "/instrumentation/nav/frequencies/selected-mhz"
            ], [
                    "nav1sby", "/instrumentation/nav/frequencies/standby-mhz"
            ], [
                    "nav1stn", "/instrumentation/nav/nav-id"
            ], [
                    "nav2use", "/instrumentation/nav[1]/frequencies/selected-mhz"
            ], [
                    "nav2sby", "/instrumentation/nav[1]/frequencies/standby-mhz"
            ], [
                    "nav2stn", "/instrumentation/nav[1]/nav-id"
            ], [
                    "adf1use", "/instrumentation/adf/frequencies/selected-khz"
            ], [
                    "adf1sby", "/instrumentation/adf/frequencies/standby-khz"
            ], [
                    "adf1stn", "/instrumentation/adf/ident"
            ], [
                    "dme1use", "/instrumentation/dme/frequencies/selected-mhz"
            ], [
                    "dme1dst", "/instrumentation/dme/indicated-distance-nm"
            ], [
                    "xpdrcod", "/instrumentation/transponder/id-code"
            ],
            // weather
            [
                    "ac-wdir", "/environment/wind-from-heading-deg"
            ], [
                    "ac-wspd", "/environment/wind-speed-kt"
            ], [
                    "ac-visi", "/environment/visibility-m"
            ], [
                    "ac-temp", "/environment/temperature-degc"
            ], [
                    "ac-dewp", "/environment/dewpoint-degc"
            ], [
                    "gnd-wdir", "/environment/config/boundary/entry/wind-from-heading-deg"
            ], [
                    "gnd-wspd", "/environment/config/boundary/entry/wind-speed-kt"
            ], [
                    "gnd-visi", "/environment/config/boundary/entry/visibility-m"
            ], [
                    "gnd-temp", "/environment/config/boundary/entry/temperature-degc"
            ], [
                    "gnd-dewp", "/environment/config/boundary/entry/dewpoint-degc"
            ], [
                    "metar-valid", "/environment/metar/valid"
            ],
    ]);

    function PhiViewModel(props) {
        var self = this;
        self.props = props;
        self.widgets = ko.observableArray([
                "METAR", "PFD", "Radiostack", "Small Map", "Stopwatch"
        ]);

        self.topics = [
                'Aircraft', 'Environment', 'Map', 'Tools', 'Simulator', 'Help',
        ];

        self.selectedTopic = ko.observable();
        self.selectedSubtopic = ko.observable();

        self.selectTopic = function(topic) {
            location.hash = topic;
        }

        self.refresh = function() {
            location.reload();
        }

        self.doPause = function() {
            fgcommand.pause();
        }

        self.doUnpause = function() {
            fgcommand.unpause();
        }

        jquery("#widgetarea").sortable({
            handle: ".widget-handle",
            axis: "y",
            cursor: "move",
        });
        jquery("#widgetarea").disableSelection();

        // Client-side routes
        Sammy(function() {
            this.get('#:topic', function() {
                self.selectedTopic( this.params.topic );
                self.selectedSubtopic('');
            });

            this.get('#:topic/:subtopic', function() {
                self.selectedTopic( this.params.topic );
                self.selectedSubtopic( this.params.subtopic );
            });
            // empty route
            this.get('', function() {
                this.app.runRoute( 'get', '#' + self.topics[0] );
            });
        }).run();

    }

    ko.components.register('Aircraft', {
        require : 'topics/Aircraft'
    });

    ko.components.register('Environment', {
        require : 'topics/Environment'
    });

    ko.components.register('Map', {
        require : 'topics/Map'
    });

    ko.components.register('Tools', {
        require : 'topics/Tools'
    });

    ko.components.register('Simulator', {
        require : 'topics/Simulator'
    });

    ko.components.register('Help', {
        require : 'topics/Help'
    });

    ko.components.register('sidebarwidget', {
        require : 'widgets/sidebarwidget'
    });

    ko.components.register('Small Map', {
        require : 'widgets/map'
    });

    ko.components.register('Radiostack', {
        require : 'widgets/radiostack'
    });

    ko.components.register('METAR', {
        require : 'widgets/metar'
    });

    ko.components.register('PFD', {
        require : 'widgets/efis'
    });

    ko.components.register('Stopwatch', {
        require : 'widgets/Stopwatch'
    });
    
    ko.components.register('dualarcgauge', {
        require: 'instruments/DualArcGauge'
    })

    ko.bindingHandlers.flotchart = {
        init : function(element, valueAccessor, allBindings) {
            // This will be called when the binding is first applied to an
            // element
            // Set up any initial state, event handlers, etc. here
            var value = valueAccessor() || {};

            if (value.hover && typeof (value.hover) === 'function') {
                jquery(element).bind("plothover", function(event, pos, item) {
                    value.hover.call(jquery(this).data("flotplot"), pos, item);
                });
            }
            if (value.click && typeof (value.click) === 'function') {
                jquery(element).bind("plotclick", function(event, pos, item) {
                    value.click.call(jquery(this).data("flotplot"), pos, item);
                });
            }
        },

        update : function(element, valueAccessor, allBindings) {
            var value = valueAccessor() || {};
            var data = ko.unwrap(value.data);
            var options = ko.unwrap(value.options);
            var plot = jquery.plot(element, data, options);
            jquery(element).data("flotplot", plot );
            var postUpdate = ko.unwrap(value.postUpdate);
            if( postUpdate ) {
                postUpdate.call( value, element );
            }

        },

    };

    ko.applyBindings(new PhiViewModel(),document.getElementById('wrapper'));

});