###############################################################################
##
##  Walk view module for FlightGear.
##
##  Inspired by the work of Stewart Andreason.
##
##  Copyright (C) 2010  Anders Gidenstam  (anders(at)gidenstam.org)
##  This file is licensed under the GPL license v2 or later.
##
###############################################################################

# Global API. Automatically selects the right walker for the current view.

# NOTE: Coordinates are always 3 component lists: [x, y, z] in meters.
# The coordinate system is the same as the main 3d model one.
# X - back, Y - right and Z - up.

# Set the forward speed of the active walker.
#   speed - walker speed in m/sec
#   Returns 1 of there is an active walker and 0 otherwise.
var forward = func (speed) {
    var cv = view.current.getPath();
    if (contains(walkers, cv)) {
        walkers[cv].forward(speed);
        return 1;
    } else {
        return 0;
    }
}

# Set the side step speed of the active walker.
#   speed - walker speed in m/sec
#   Returns 1 of there is an active walker and 0 otherwise.
var side_step = func (speed) {
    var cv = view.current.getPath();
    if (contains(walkers, cv)) {
        walkers[cv].side_step(speed);
        return 1;
    } else {
        return 0;
    }
}

# Get the currently active walker.
#   Returns the active walker object or nil otherwise.
var active_walker = func {
    var cv = view.current.getPath();
    if (contains(walkers, cv)) {
        return walkers[cv];
    } else {
        return nil;
    }
}

###############################################################################
# The walker class.
# ==============================================================================
# Class for a moving view.
#
# CONSTRUCTOR:
#       walker.new(<view name>, <constraints>, <managers>);
#
#         view name    ... The name of the view     : string
#         constraints  ... The movement constraints : constraint hash
#                          Determines where the view can go.
#         managers     ... Optional list of custom managers. A manager is a
#                          a hash that contains an update function of the type
#                          func(walker instance). The update function
#                          of each manager will be called as the last part of
#                          each walker update. Intended for controlling a
#                          a 3d model or similar.
#
# METHODS:
#       active() : bool
#         returns true if this walk view is active.
#
#       forward(speed)
#           Sets the forward speed of this walk view.
#         speed  ... speed in m/sec : double
#
#       side_step(speed)
#           Sets the side step speed of this walk view.
#         speed  ... speed in m/sec : double
#
#       set_pos(pos)
#         pos ... position in meter : [double, double, double]
#       get_pos() : position ([meter, meter, meter])
#
#       set_eye_height(h)
#       get_eye_height() : int (meter)
#
#       set_constraints(constraints)
#       get_constraints() : constraint hash
#
# EXAMPLE:
#       var constraint =
#           walkview.slopingYAlignedPlane.new([19.1, -0.3, -8.85],
#                                             [19.5,  0.3, -8.85]);
#       var walker = walkview.walker.new("Passenger View", constraint);
#
#       See Aircraft/Nordstern, Aircraft/Short_Empire and Aircraft/ZLT-NT
#       for working examples of walk views.
#
# NOTES:
#       Currently there can only be one view manager per view so the
#       walk view should not have any other view manager.
var Walker = {
    new : func (view_name, constraints = nil, managers = nil) {
        var obj = { parents : [Walker] };
        obj.view_name   = view_name;
        obj.view        = view.views[view.indexof(view_name)];
        obj.constraints = constraints;
        obj.managers    = managers;
        obj.position    = [
            obj.view.getNode("config/z-offset-m").getValue(),
            obj.view.getNode("config/x-offset-m").getValue(),
            obj.view.getNode("config/y-offset-m").getValue()
            ];
        obj.heading =
            obj.view.getNode("config/heading-offset-deg").getValue();
        obj.speed_fwd  = 0.0;
        obj.speed_side = 0.0;
        obj.isactive = 0;
        obj.eye_height  = 1.60;
        obj.goal_height = obj.position[2] + obj.eye_height;

        # Register this walker.
        view.manager.register(view_name, obj);
        walkers[obj.view.getPath()] = obj;

        #debug.dump(obj);
        return obj;
    },
    active : func {
        return me.isactive;
    },
    forward : func (speed) {
        me.speed_fwd = speed;
    },
    side_step : func (speed) {
        me.speed_side = speed;
    },
    set_pos : func (pos) {
        me.position[0] = pos[0];
        me.position[1] = pos[1];
        me.position[2] = pos[2];
    },
    get_pos : func {
        return [me.position[0], me.position[1], me.position[2]];
    },
    set_eye_height : func (h) {
        me.eye_height = h;
    },
    get_eye_height : func {
        return me.eye_height;
    },
    set_constraints : func (constraints) {
        me.constraints = constraints;
    },
    get_constraints : func {
        return me.constraints;
    },
    # View handler implementation.
    init : func {
    },
    start  : func {
        me.isactive = 1;
        me.last_time = getprop("/sim/time/elapsed-sec") - 0.0001;
        me.update();
        me.position[2] = me.goal_height;
    },
    stop   : func {
        me.isactive = 0;
    },
    # The update function is called by the view manager when the view is active.
    update : func {
        var t  = getprop("/sim/time/elapsed-sec");
        var dt = t - me.last_time;
        if (dt == 0.0) return;

        var cur = props.globals.getNode("/sim/current-view");
        me.heading = cur.getNode("heading-offset-deg").getValue();

        var new_pos =
            [me.position[0] -
             (me.speed_fwd  * dt * math.cos(me.heading * TO_RAD) +
              me.speed_side * dt * math.sin(me.heading * TO_RAD)),
             me.position[1] -
             (me.speed_fwd  * dt * math.sin(me.heading * TO_RAD) -
              me.speed_side * dt * math.cos(me.heading * TO_RAD)),
             me.position[2]];

        var cur_height = me.position[2];
        if (me.constraints != nil) {
            new_pos = me.constraints.constrain(new_pos);
            if (new_pos == NO_POS) {
                printlog("warn",
                         "WalkView: Constraint for " ~ me.view_name ~
                         " returned NO_POS.");
            } else {
                me.position     = new_pos;
                me.goal_height  = me.position[2] + me.eye_height;
            }
        }
        # Change the view height smoothly
        if (math.abs(me.goal_height - cur_height) > 2.0 * dt) {
            me.position[2] =
                cur_height +
                2.0 * dt *
                ((me.goal_height > cur_height) ? 1 : -1);
        } else {
            me.position[2] = me.goal_height;
        }

        cur.getNode("z-offset-m").setValue(me.position[0]);
        cur.getNode("x-offset-m").setValue(me.position[1]);
        cur.getNode("y-offset-m").setValue(me.position[2]);

        if (me.managers != nil) {
            foreach(var m; me.managers) {
                m.update(me);
            }
        }

        me.last_time = t;
        return 0.0;
    },
};

###############################################################################
# Constraint classes. Determines where the view can walk.
#

# Convenience functions.

# Build a UnionConstraint hierarchy from a list of constraints.
#   cs - list of constraints : [constraint]
var makeUnionConstraint = func (cs) {
    if (size(cs) < 2) return cs[0];
    
    var ret = cs[0];
    for (var i = 1; i < size(cs); i += 1) {
        ret = UnionConstraint.new(ret, cs[i]);
    }
    return ret;
}

# Build a UnionConstraint hierachy that represents a polyline path
# with a certain width. Each internal point gets a circular surface.
#   points     - list of points    : [position] ([[meter, meter, meter]])
#   width      - width of the path : length   (meter)
#   round_ends - put a circle also on the first and last points : bool
var makePolylinePath = func (points, width, round_ends = 0) {
    if (size(points) < 2) return nil;
    var ret = LinePlane.new(points[0], points[1], width);
    if (round_ends) {
        ret = UnionConstraint.new(ret,
                                  CircularXYSurface.new(points[0], width/2));
    }
    for (var i = 2; i < size(points); i += 1) {
        var line = LinePlane.new(points[i-1], points[i], width);
        if (i + 1 < size(points) or round_ends) {
            line = UnionConstraint.new
                       (line,
                        CircularXYSurface.new(points[i], width/2));
        }
        ret = UnionConstraint.new(line, ret);
    }
    return ret;
}

# The union of two constraints.
#   c1, c2 - the constraints : constraint
# NOTE: Assumes that the constraints are convex.
var UnionConstraint = {
    new : func (c1, c2) {
        var obj = { parents : [UnionConstraint] };
        obj.c1 = c1;
        obj.c2 = c2;
        return obj;
    },
    constrain : func (pos) {
        var p1 = me.c1.constrain(pos);
        var p2 = me.c2.constrain(pos);
        if (p1[0] == pos[0] and p1[1] == pos[1]) {
            return p1;
        } elsif (p2[0] == pos[0] and p2[1] == pos[1]) {
            return p2;
        } else {
            if (closerXY(pos, p1, p2) <= 0) {
                return p1;
            } else {
                return p2;
            }
        }
    }
};

# Rectangular plane defined by a straight line and a width.
# The line is extruded horizontally on each side by width/2 into a
# planar surface.
#   p1, p2 - the line endpoints.       : position ([meter, meter, meter])
#   width  - total width of the plane. : length   (meter)
var LinePlane = {
    new : func (p1, p2, width) {
        var obj = { parents : [LinePlane] };
        obj.p1         = p1;
        obj.p2         = p2;
        obj.halfwidth  = width/2;
        obj.length     = vec2.length(vec2.sub(p2, p1));
        obj.e1         = vec2.normalize(vec2.sub(p2, p1));
        obj.e2         = [obj.e1[1], -obj.e1[0]];
        obj.k          = (p2[2] - p1[2]) / obj.length;

        return obj;
    },
    constrain : func (pos) {
        var p      = [pos[0], pos[1], pos[2]];
        var pXY    = vec2.sub(pos, me.p1);
        var along  = vec2.dot(pXY, me.e1);
        var across = vec2.dot(pXY, me.e2);

        var along2  = max(0, min(along, me.length));
        var across2 = max(-me.halfwidth, min(across, me.halfwidth));
        if (along2 != along or across2 != across) {
            # Compute new XY position.
            var t = vec2.add(vec2.mul(along2, me.e1), vec2.mul(across2, me.e2));
            p[0] = me.p1[0] + t[0];
            p[1] = me.p1[1] + t[1];
        }

        # Compute Z positition.
        p[2] = me.p1[2] + me.k * along2;
        return p;
    }
};

# Circular surface aligned with the XY plane
#   center - the center point       : position ([meter, meter, meter])
#   radius - radius in the XY plane : length   (meter)
var CircularXYSurface = {
    new : func (center, radius) {
        var obj = { parents : [CircularXYSurface] };
        obj.center     = center;
        obj.radius     = radius;

        return obj;
    },
    constrain : func (pos) {
        var p   = [pos[0], pos[1], me.center[2]];
        var pXY = vec2.sub(pos, me.center);
        var lXY = vec2.length(pXY);

        if (lXY > me.radius) {
            var t = vec2.add(me.center, vec2.mul(me.radius/lXY, pXY));
            p[0] = t[0];
            p[1] = t[1];
        }
        return p;
    },
};

# Mostly aligned plane sloping along the X axis.
# NOTE: Obsolete. Use linePlane instead.
#   minp - the X,Y minimum point : position ([meter, meter, meter])
#   maxp - the X,Y maximum point : position ([meter, meter, meter])
var SlopingYAlignedPlane = {
    new : func (minp, maxp) {
        return LinePlane.new([minp[0], (minp[1] + maxp[1])/2, minp[2]],
                             [maxp[0], (minp[1] + maxp[1])/2, maxp[2]],
                             (maxp[1] - minp[1]));
    }
};

# Action constraint
#   Triggers an action when entering or exiting the constraint.
#   constraint      - the area in question : constraint
#   on_enter()      - function that is called when the walker enters the area.
#   on_exit(x, y)   - function that is called when the walker leaves the area.
#                     x and y are <0, 0 or >0 depending on in which direction(s)
#                     the walker left the constraint.
var ActionConstraint = {
    new : func (constraint, on_enter = nil, on_exit = nil) {
        var obj = { parents : [ActionConstraint] };
        obj.constraint = constraint;
        obj.on_enter   = on_enter;
        obj.on_exit    = on_exit;
        obj.inside     = 0;
        return obj;
    },
    constrain : func (pos) {
        var p = me.constraint.constrain(pos);
        if (p[0] == pos[0] and p[1] == pos[1]) {
            if (!me.inside) {
                me.inside = 1;
                if (me.on_enter != nil) {
                    me.on_enter();
                }
            }
        } else {
            if (me.inside) {
                me.inside -= 1;
                if (!me.inside and me.on_exit != nil) {
                    me.on_exit(pos[0] - p[0], pos[1] - p[1]);
                }
            }
        }
        return p;
    }
};

# Conditional constraint
#   The area is only available when the predicate function returns true.
#   constraint      - the area in question : constraint
#   predicate()     - boolean function that determines if the area is available.
var ConditionalConstraint = {
    new : func (constraint, predicate = nil) {
        var obj = { parents : [ConditionalConstraint] };
        obj.constraint = constraint;
        obj.predicate  = predicate;
        return obj;
    },
    constrain : func (pos) {
        if (me.predicate == nil or me.predicate()) {
            return me.constraint.constrain(pos);
        } else {
            return NO_POS;
        }
    }
};


###############################################################################
# Manager classes.

# JSBSim pointmass manager.
#   Moves a pointmass representing the crew member together with the view.
# CONSTRUCTOR:
#       JSBSimPointmass.new(<pointmass index>);
#
#         pointmass index ... The index of the pointmass : int
#         offsets         ... [x, y ,z] position in meter of the origin of the
#                             JSBSim structural frame in the 3d model frame. 
#
# NOTE: Only supports aligned frames (yet).
#
var JSBSimPointmass = {
    new : func (index, offsets = nil) {
        var base = props.globals.getNode("fdm/jsbsim/inertia");
        var prefix  = "pointmass-location-";
        var postfix = "-inches[" ~ index ~"]";
        var obj = { parents : [JSBSimPointmass] };
        obj.pos_ft =
            [
             base.getNode(prefix ~ "X" ~ postfix),
             base.getNode(prefix ~ "Y" ~ postfix),
             base.getNode(prefix ~ "Z" ~ postfix)
            ];
        obj.offset = (offsets == nil) ? [0.0, 0.0, 0.0] : offsets;
        return obj;
    },
    update : func (walker) {
        var pos = walker.get_pos();
        pos[2] += walker.get_eye_height()/2;
        forindex (var i; pos) {
            me.pos_ft[i].setValue((pos[i] - me.offset[i])*M2FT*12);
        }
    }
};

###############################################################################
# Module implementation below

var TO_RAD = math.pi/180;
var TO_DEG = 180/math.pi;

var NO_POS = [-9999.0, -9999.0, -9999.0];

var walkers = {};

var closerXY = func (pos, p1, p2) {
    var l1 = [p1[0] - pos[0], p1[1] - pos[1]];
    var l2 = [p2[0] - pos[0], p2[1] - pos[1]];
    return (l1[0]*l1[0] + l1[1]*l1[1]) - (l2[0]*l2[0] + l2[1]*l2[1]);
}

var max = func (a, b) {
    return b > a ? b : a;
}
var min = func (a, b) {
    return a > b ? b : a;
}

# 2D vector math.
var vec2 = {
    add : func (a, b) {
        return [a[0] + b[0], a[1] + b[1]];
    },
    sub : func (a, b) {
        return [a[0] - b[0], a[1] - b[1]];
    },
    mul : func (k, a) {
        return [k * a[0], k * a[1]];
    },
    length : func (a) {
        return math.sqrt(a[0]*a[0] + a[1]*a[1]);
    },
    dot : func (a, b) {
        return a[0]*b[0] + a[1]*b[1];
    },
    normalize : func (a) {
        var s = 1/vec2.length(a);
        return [s * a[0], s * a[1]];
    }
}