//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

package com.threerings.flash {

import flash.display.Shape;
import flash.display.Sprite;

import flash.events.AsyncErrorEvent;
import flash.events.IOErrorEvent;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.events.NetStatusEvent;
import flash.events.SecurityErrorEvent;
import flash.events.TimerEvent;

import flash.media.Video;

import flash.net.NetConnection;
import flash.net.NetStream;

import flash.utils.Timer;

import com.threerings.util.ValueEvent;

public class VideoDisplayer extends Sprite
{
    /** A value event dispatched when the size of the video is known.
     * Value: [ width, height ]. */
    public static const SIZE_KNOWN :String = "videoDisplayerSizeKnown";

    /** A value event sent when there's an error loading the video.
     * Value: original error event. */
    public static const VIDEO_ERROR :String = "videoDisplayerError";

    /**
     * Create a video displayer.
     */
    public function VideoDisplayer ()
    {
        _vid = new Video();
        addChild(_vid);

        _mask = new Shape();
        this.mask = _mask;
        addChild(_mask);

        addEventListener(MouseEvent.ROLL_OVER, handleRollOver);
        addEventListener(MouseEvent.ROLL_OUT, handleRollOut);

        _videoChecker.addEventListener(TimerEvent.TIMER, handleVideoCheck);

        _pauser = new Sprite();
        _pauser.addEventListener(MouseEvent.CLICK, handleClick);
        redrawPauser();
    }

    /**
     * Start playing a video!
     */
    public function setup (url :String) :void
    {
        _netCon = new NetConnection();
        _netCon.addEventListener(NetStatusEvent.NET_STATUS, handleNetStatus);

        // error handlers
        _netCon.addEventListener(AsyncErrorEvent.ASYNC_ERROR, handleAsyncError);
        _netCon.addEventListener(IOErrorEvent.IO_ERROR, handleIOError);
        _netCon.addEventListener(SecurityErrorEvent.SECURITY_ERROR, handleSecurityError);

        _netCon.connect(null);
        _netStream = new NetStream(_netCon);
        // pass in refs to some of our protected methods
        _netStream.client = {
            onMetaData: metaDataReceived
        };
        _netStream.addEventListener(NetStatusEvent.NET_STATUS, handleStreamNetStatus);

        _vid.attachNetStream(_netStream);
        _videoChecker.start();
        _netStream.play(url);
        _paused = false;
    }

    /**
     * Stop playing our video.
     */
    public function shutdown () :void
    {
        _videoChecker.reset();
        _vid.attachNetStream(null);

        if (_netStream != null) {
            _netStream.close();
            _netStream.removeEventListener(NetStatusEvent.NET_STATUS, handleStreamNetStatus);
            _netStream = null;
        }
        if (_netCon != null) {
            _netCon.close();
            _netCon.removeEventListener(NetStatusEvent.NET_STATUS, handleNetStatus);
            _netCon.removeEventListener(AsyncErrorEvent.ASYNC_ERROR, handleAsyncError);
            _netCon.removeEventListener(IOErrorEvent.IO_ERROR, handleIOError);
            _netCon.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, handleSecurityError);
            _netCon = null;
        }
    }

    /**
     * Check to see if we now know the dimensions of the video.
     */
    protected function handleVideoCheck (event :TimerEvent) :void
    {
        if (_vid.videoWidth == 0 || _vid.videoHeight == 0) {
            return; // not known yet!
        }

        // stop the checker timer
        _videoChecker.stop();

        // set up the width/height
        _vid.width = _vid.videoWidth;
        _vid.height = _vid.videoHeight;

        // set up our mask to be the size of the video so that we're
        // guaranteed to get the mouse clicks
        with (_mask.graphics) {
            clear();
            beginFill(0xFFFFFF);
            drawRect(0, 0, _vid.videoWidth, _vid.videoHeight);
            endFill();
        }

        redrawPauser();

        // tell any interested parties
        dispatchEvent(new ValueEvent(SIZE_KNOWN, [ _vid.videoWidth, _vid.videoHeight ]));
    }

    protected function handleRollOver (event :MouseEvent) :void
    {
        addChild(_pauser);
    }

    protected function handleRollOut (event :MouseEvent) :void
    {
        if (_pauser.parent) {
            removeChild(_pauser);
        }
    }

    protected function handleClick (event :MouseEvent) :void
    {
        // the buck stops here!
        event.stopImmediatePropagation();

        if (_paused) {
            _netStream.resume();
            _paused = false;

        } else {
            _netStream.pause();
            _paused = true;
        }
        redrawPauser();
    }

    /**
     * Draw the pauser sprite, update it's location.
     * This will become something artistic, etc.
     */
    protected function redrawPauser () :void
    {
        with (_pauser.graphics) {
            clear();
            // draw a nice circle
            beginFill(0x333333);
            drawCircle(0, 0, 20);
            endFill();
            lineStyle(2, 0);
            drawCircle(0, 0, 20);

            if (_paused) {
                lineStyle(0, 0, 0);
                beginFill(0x00FF00);
                moveTo(-4, -10);
                lineTo(4, 0);
                lineTo(-4, 10);
                lineTo(-4, -10);
                endFill();

            } else {
                lineStyle(2, 0x00FF00);
                moveTo(-4, -10);
                lineTo(-4, 10);
                moveTo(4, -10);
                lineTo(4, 10);
            }
        }

        // and update the location
        _pauser.x = _vid.width/2;
        _pauser.y = _vid.height/2;
    }

    protected function handleNetStatus (event :NetStatusEvent) :void
    {
        var info :Object = event.info;
        if ("error" == info.level) {
            trace("NetStatus error: " + info.code);
            redispatchError(event);
            return;
        }
        // else info.level == "status"
        switch (info.code) {
        case "NetConnection.Connect.Success":
        case "NetConnection.Connect.Closed":
            // these status events we ignore
            break;

        default:
            trace("NetStatus status: " + info.code);
            break;
        }
    }

    protected function handleStreamNetStatus (event :NetStatusEvent) :void
    {
        if (event.info.code == "NetStream.Play.Stop") {
            _netStream.seek(0);
            _netStream.pause();
            _paused = true;
            redrawPauser();
        }
    }

    protected function handleAsyncError (event :AsyncErrorEvent) :void
    {
        trace("AsyncError: " + event);
        redispatchError(event);
    }

    protected function handleIOError (event :IOErrorEvent) :void
    {
        trace("IOError: " + event);
        redispatchError(event);
    }

    protected function handleSecurityError (event :SecurityErrorEvent) :void
    {
        trace("SecurityError: " + event);
        redispatchError(event);
    }

    /**
     * Redispatch some error we received to our listeners.
     */
    protected function redispatchError (event :Event) :void
    {
        dispatchEvent(new ValueEvent(VIDEO_ERROR, event));
    }

    /**
     * Called when metadata (if any) is found in the video stream.
     */
    protected function metaDataReceived (obj :Object) :void
    {
        trace("Got video metadata:");
        for (var n :String in obj) {
            trace("    " + n + ": " + obj[n]);
        }
    }
    
    protected var _vid :Video;

    /** Our mask, also defines our boundaries for clicking. */
    protected var _mask :Shape;

    protected var _paused :Boolean = false;

    protected var _netCon :NetConnection;

    protected var _netStream :NetStream;

    protected var _pauser :Sprite;

    /** Checks the video every 100ms to see if the dimensions are now know.
     * Yes, this is how to do it. We could trigger on ENTER_FRAME, but then
     * we may not know the dimensions unless we're added on the display list,
     * and we want this to work in the general case. */
    protected var _videoChecker :Timer = new Timer(100);
}
}
