#!/usr/bin/env python
#
# HTTP streaming cache.
#
# Copyright (C) 2004  Andreas Junghans <aj@lucid-cake.net>
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program 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 General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#


#import gc
import socket
import sys
import time
import SimpleHTTPServer
from SimpleHTTPServer import SimpleHTTPRequestHandler
from SocketServer import TCPServer
from SocketServer import ThreadingTCPServer
from threading import Lock
from threading import Semaphore
from threading import Thread
from myurllib2 import urlopen
from myurllib2 import Request
from Queue import Queue

_server = None
_cacheSize = 512*1024       # default cache size: 512 KB
_chunkSize = 64*1024        # size of chunks to read and write
_bitrateChunks = 32
_statusQuery = "status"     # the resource to return status information
_prepareCacheQuery = "prepare/" # the resource to prepare caching of an URL
_cacheEmptyPrefix = None
_calculateBitrate = 0
_buffers = []
_lock = Lock()


class StreamBuffer:
    """
    Class for buffering an HTTP stream.
    """

    def __init__(self, url):
        """
        Initializes this instance.
        """
        self.url = url
        #self.chunkToRead = 0
        #self.chunkToFill = 0
        self.chunkCount = _cacheSize/_chunkSize
        self.queue = Queue(self.chunkCount)
        #self.chunks = [None] * self.chunkCount
        #self.chunkTimes = [0.0] * self.chunkCount
        self.bufferFull = Semaphore(0)
        self.initialFillComplete = 0
        #self.okToRead = Semaphore(0)
        #self.okToFill = Semaphore(self.chunkCount)
        self.lock = Lock()
        self.exit = 0
        self.dead = 0
        self.metainfo = None
        self.startTime = 0
        self.chunksRead = 0
        self.bitrate = 0
        

    def readChunk(self, handle):
        """
        Reads a single chunk from handle and places it in the buffer. If the
        buffer is full, this method waits for the fillBuffer semaphore until
        it gets it. If the buffer is still full, it waits again, etc.
        """
        exit = 0
        data = ""
        while len(data) < _chunkSize and not exit:
            #sizeBefore = len(data)
            data += handle.read(_chunkSize-len(data))   # blocking, but with a timeout
                                                        # (I think 60 seconds by default)
                                                        # TODO: test the timeout (using a
                                                        # closed live TV window)
            #print str(len(data)-sizeBefore)
            if data == "":
                exit = 1    # EOF
            #else:
            #    self.lock.acquire()
            #    exit = self.exit
            #    self.lock.release()
        if not exit:
            #self.okToFill.acquire()
            self.lock.acquire()
            exit = self.exit
            if exit:
                self.lock.release()
            else:
                #self.chunks[self.chunkToFill] = data
                #self.chunkTimes[self.chunkToFill] = currentTime
                if _calculateBitrate:
                    self.chunksRead += 1
                    if self.startTime == 0:
                        self.startTime = time.time()
                    else:
                        timediff = time.time()-self.startTime
                        if timediff != 0:
                            self.bitrate = self.chunksRead*_chunkSize*8/1024.0/timediff
                #self.chunkToFill = (self.chunkToFill + 1)%self.chunkCount
                self.lock.release()
                self.queue.put(data, 1)     # blocking
                #self.okToRead.release()
        return exit
        
        
    def __call__(self):
        """
        Run the thread.
        """
        try:
            print "Thread for url '" + str(self.url) + "' started"
            sys.stdout.flush()
            _lock.acquire()
            for buffer in _buffers:
                buffer.lock.acquire()
                if buffer.url == self.url:
                    # kill existing request for the same URL
                    buffer.exit = 1
                    buffer.dead = 1
                buffer.lock.release()
            _buffers.append(self)
            _lock.release()
            req = Request(self.url, None, {'X-CyTV-Cache': 'stream'})
            handle = urlopen(req)
            self.metainfo = handle.info()
            index = 0
            exit = 0
            while index < self.chunkCount and not exit:
                exit = self.readChunk(handle)
                #if index == 0 and not exit:
                #    time.sleep(1)
                index += 1
            self.bufferFull.release()
            self.lock.acquire()
            self.initialFillComplete = 1
            self.lock.release()
            #if self.url.find("noCacheDelay") == -1:
            #    time.sleep(3)   # wait before giving away the cache contents
            #    print "delaying cache contents"
            #    sys.stdout.flush()
            #if _cacheFullPrefix != None:
            #    try:
            #        cacheFullReq = Request(_cacheFullPrefix + self.url, None, {'X-CyTV-Cache': 'cacheFull'})
            #        #print "requesting '" + _cacheFullPrefix + self.url + "'"
            #        #sys.stdout.flush()
            #        cacheFullHandle = urlopen(cacheFullReq)
            #    except:
            #        pass
            #    try:
            #        cacheFullHandle.close()
            #    except:
            #        pass
            while not exit:
                exit = self.readChunk(handle)
        finally:
            self.lock.acquire()
            self.dead = 1
            self.lock.release()
            self.bufferFull.release()   # in case there was an exception,
                                        # make sure the server thread
                                        # wakes up
            #self.okToRead.release()
            try:
                self.queue.put("", 0)   # non-blocking; just to make sure
                                        # the server thread is awake
            except:
                pass
            print "Thread for url '" + str(self.url) + "' stopped"
            sys.stdout.flush()
            _lock.acquire()
            _buffers.remove(self)
            _lock.release()
            try:
                handle.close()
            except:
                pass
        
    
class MyHTTPServer(ThreadingTCPServer):
    """
    HTTP server class (based on BaseHTTPServer.py; most notably, it's
    derived from ThreadingTCPServer to handle more than one request).
    """

    allow_reuse_address = 1    # Seems to make sense in testing environment
    daemon_threads = 1         # allows (somewhat) clean shutdown


    def server_bind(self):
        """Override server_bind to store the server name."""
        TCPServer.server_bind(self)
        host, port = self.socket.getsockname()
        self.server_name = socket.getfqdn(host)
        self.server_port = port
    

    def serve_forever(self):
        """
        Handles request forever (at least until we quit).
        """
        while 1:
            try:
                self.handle_request()
            except ServerExitException:
                self.server_close()
                break


class MyHTTPRequestHandler(SimpleHTTPRequestHandler):
    """
    Handler for HTTP requests. There is one instance of this per client.
    """

    def setup(self):
        #print "setup"
        self.connection = self.request
        self.rfile = self.connection.makefile('rb', -1)
        self.wfile = self.connection.makefile('wb', 8192)


    def do_GET(self):
        """
        Handles GET requests.
        TODO: Test - what happens with POSTs? What's the default behaviour
                     of SimpleHTTPRequestHandler?
        """
        requestHandled = 0
        if len(self.path) > 1:
            if self.path[1:] == _statusQuery:
                response = ""
                _lock.acquire()
                response += "* "
                for buffer in _buffers:
                    buffer.lock.acquire()
                    response += "URL: '" + str(buffer.url)
                    response += "'; cache size: " + str(buffer.chunkCount*_chunkSize)
                    #fillState = buffer.chunkToFill - buffer.chunkToRead
                    #if fillState == 0:
                    #    if buffer.chunks[buffer.chunkToFill] == None:
                    #        fileState = 0.0
                    #    else:
                    #        fillState = 100.0
                    #else:
                    #    fillState = (fillState%buffer.chunkCount)*100.0/buffer.chunkCount
                    fillState = buffer.queue.qsize()*100.0/buffer.chunkCount
                    response += "; fill state: " + str(fillState)
                    response += "; initialized: " + str(buffer.initialFillComplete)
                    response += "; bitrate: " + str(buffer.bitrate) + "\n"
                    buffer.lock.release()
                _lock.release()
                self.wfile.write("HTTP/1.1 200 OK\r\n")
                self.send_header("Content-Type", "text/plain;charset=utf-8")
                self.send_header("Content-Length", str(len(response)))
                self.end_headers()
                self.wfile.write(response)
                requestHandled = 1
            #elif self.path[1:1+len(_prepareCacheQuery)] == _prepareCacheQuery:
            #    requestHandled = 1
            #    actualPath = _server + self.path[0] + self.path[1+len(_prepareCacheQuery):]
            #    print "Preparing buffer for %s" % actualPath
            #    sys.stdout.flush()
            #    buffer = StreamBuffer(actualPath)
            #    thread = Thread(target=buffer)
            #    thread.start()
            elif self.path[1:] == "junk":
                print "sending junk"
                sys.stdout.flush()
                requestHandled = 1
                self.wfile.write("HTTP/1.1 200 OK\r\n")
                self.send_header("junk", "junk")
                self.end_headers()
                junk = "\x00"*1024
                for i in range(1024):
                    self.wfile.write(junk)
                
        if not requestHandled:
            actualPath = _server + self.path
            
            # test if there's already a suitable buffer
            _lock.acquire()
            #for buffer in _buffers:
            #    if buffer.url == actualPath:
            #        _lock.release()
            #        print "Existing buffer found for %s" % actualPath
            #        sys.stdout.flush()
            #        break
            #else:
            if 1:
                _lock.release()
                # create a new buffer and forward to the destination server
                #print "Creating new buffer for %s" % actualPath
                #sys.stdout.flush()
                buffer = StreamBuffer(_server + self.path)
                thread = Thread(target=buffer)
                thread.start()
            
            try:
                try:
                    buffer.bufferFull.acquire() # wait for the buffer to be completely filled
                    dead = 0
                    emptyCounter = 0
                    while not dead:
                        chunk = buffer.queue.get(1)     # blocking
                        if _cacheEmptyPrefix is not None and buffer.queue.qsize() == 0:
                            emptyCounter += 1
                            if emptyCounter%8 == 0:
                                print "empty cache"
                                sys.stdout.flush()
                                try:
                                    cacheEmptyReq = Request(_cacheEmptyPrefix + actualPath, None, {'X-CyTV-Cache': 'cacheEmpty'})
                                    print "requesting '" + _cacheEmptyPrefix + actualPath + "'"
                                    sys.stdout.flush()
                                    cacheEmptyHandle = urlopen(cacheEmptyReq)
                                except:
                                    pass
                                try:
                                    cacheEmptyHandle.close()
                                except:
                                    pass

                        #buffer.okToRead.acquire()
                        buffer.lock.acquire()
                        dead = buffer.dead
                        if dead:
                            buffer.lock.release()
                        else:
                            if buffer.metainfo is not None:
                                try:
                                    self.wfile.write("HTTP/1.1 200 OK\r\n")
                                    for header in buffer.metainfo.headers:
                                        self.wfile.write(header)    # TODO: make sure these contain \r\n
                                    self.end_headers()
                                except:
                                    buffer.lock.release()
                                    raise
                                buffer.metainfo = None
                            #chunk = buffer.chunks[buffer.chunkToRead]
                            #buffer.chunks[buffer.chunkToRead] = None
                            #buffer.chunkToRead = (buffer.chunkToRead + 1)%buffer.chunkCount
                            buffer.lock.release()
                            #buffer.okToFill.release()
                            self.wfile.write(chunk)
                finally:
                    buffer.lock.acquire()
                    buffer.exit = 1
                    buffer.lock.release()
                    #buffer.okToFill.release() # make sure the thread wakes up
                    try:
                        junk = buffer.queue.get(0)  # non-blocking, just to be sure
                                                    # the thread is awake
                    except:
                        pass
            except:
                if sys.exc_info()[0] == IOError or sys.exc_info()[0] == socket.error:
                    pass    # ignore these (they are normal)
                else:
                    raise
                

if __name__ == "__main__":
    
    #gc.set_debug(gc.DEBUG_LEAK + gc.DEBUG_STATS);
    print "CyTV stream cache started at " + time.strftime("%Y-%m-%d %H:%M:%S")
    print "================================================"
    print
    sys.stdout.flush()
    try:
        # sys.argv[1] is the port that the cache should listen on
        # (evaluated by SimpleHTTPServer.test())
        _server = sys.argv[2]   # this must be a URL like "http://server"
                                # or "http://server:port" (without trailing slash
                                # since the requested resource is appended and
                                # always starts with a slash)
        _cacheSize = int(sys.argv[3])
        if _cacheSize < _chunkSize*2:
            _cacheSize = _chunkSize*2
        else:
            numChunks = (_cacheSize-1)/_chunkSize + 1
            _cacheSize = numChunks*_chunkSize
        _statusQuery = sys.argv[4]
        _prepareCacheQuery = sys.argv[5]
        _calculateBitrate = int(sys.argv[6])
        _cacheEmptyPrefix = sys.argv[7]
    except:
        pass

    try:
        SimpleHTTPServer.test(MyHTTPRequestHandler, MyHTTPServer)
    except:
        #raise
        print "HTTP cache quitting"
        sys.stdout.flush()

