/*
 * Base class for data formatting
 */
function Formatter() {

  this.format = function(data) {
  
    return data;
  };
  
  this.toString = function() { return "[object Formatter]" };
}

/*
 * Formatter for a duration
 */
function DurationFormatter() {

  this.format = function(value) {
  
    var span = new TimeSpan(value);
    var text = "";
    
    if (span.days > 0) {
    
      text += span.days + "d, ";
    }
    
    // Always show if at least 1 day
    if (span.hours > 0 || text) {
    
      text += span.hours + ":" +
              (span.minutes < 10 ? "0" : "") + span.minutes + ":" +
              (span.seconds < 10 ? "0" : "") + span.seconds;
    } else {
    
      text += span.minutes + ":" +
              (span.seconds < 10 ? "0" : "") + span.seconds;
    }
    
    return text;
  };
  
  this.toString = function() { return "[object DurationFormatter]" };
}
DurationFormatter.prototype = new Formatter;

/************************* Resource formatters ************************/

/*
 * Base class for resource display formatting
 */
function ResourceFormatter() {

  this.format = function(resource) {
  
    var $item = $("<embed/>");
    
    $item.attr("src", resource.fullURL);
    
    if (resource.properties.type) {
    
      $item.attr("type", resource.properties.type);
    }
  
    return $item;
  };
  
  this.toString = function() { return "[object ResourceFormatter]" };
}
ResourceFormatter.prototype = new Formatter;

/*
 * Formatter for images
 */
function ImageFormatter() {

  var types = {
    "application/msword": "document.png",
    "application/rar": "package.png",
    "application/x-executable": "executable.png",
    "application/x-msdos-program": "executable.png",
    "application/x-msi": "executable.png",
    "application/xhtml+xml": "html.png",
    "application/pdf": "document.png",
    "application/zip": "package.png",
    "audio": "audio.png",
    "image": "image.png",
    "text": "text.png",
    "text/html": "html.png",
    "text/rtf": "document.png",
    "video": "video.png"
  };
  
  function guessFallback(type) {
  
    var fallback = "images/thumbnails/generic.png";
      
    if (typeof type !== "undefined") {
    
      var type = type.split("/")[0];
      
      if (type in types) {
      
        fallback = "images/thumbnails/" + types[type];
      }
    }
    
    return fallback;
  }

  this.format = function(resource, value) {
  
    var $image = $("<img/>");
    var type = resource.properties.type;
    
    if (!value) {
    
      var value = guessFallback(type);
    } else if (!/^[a-z]+:\/\//.test(value)) {
    
      // Ensure absolute URL
      value = resource.source.url + "/" + value;
    }
    
    $image.addClass("loading")
          .bind("load", function() {
          
            $(this).removeClass("loading");
          })
          // Automatically switch to fallback if image loading fails
          .bind("error", {type: type}, function(e) {
          
            $(this).attr("src", guessFallback(e.data.type));
          })
          .attr("src", value)
          // Images are draggable by default
          .attr("draggable", false);
    
    return $image;
  };
  
  this.toString = function() { return "[object ImageFormatter]" };
}
ImageFormatter.prototype = new PropertyFormatter;

/*
 * Resource display formatter for media types
 */
// TODO: player controls
function MediaResourceFormatter() {

  this.createPlayer = function($element) {
  
    
  }
}
MediaResourceFormatter.prototype = new ResourceFormatter;

/*
 * Resource display formatter for audio types
 */
function AudioResourceFormatter() {

  this.format = function(resource) {
  
    var $item = $("<div/>", {"class": "playerwrapper audio"});
    var $player = $("<p/>", {"class": "player audio"});
    var $audio = $("<audio/>");
    var player = $audio[0];
    var $equalizer = $("<canvas/>", {"class": "equalizer"});
    var $playPauseButton = $("<button/>", {"class": "action play"});
    var $volumeWidget = $(new VolumeWidget(1)).addClass("volumebar");
    var $muteButton = $("<button/>", {"class": "action volume mute"});
    var $seekProgress = $(new ProgressSeekWidget(player));
    var $durationLabel = $("<span/>", {"class": "duration"});
    var durationFormatter = new DurationFormatter();
    
    $playPauseButton.text("Wiedergabe")
                    .attr("title", "Wiedergabe starten")
                    .click(function() {
                    
                      if (player.paused || player.ended) {
                      
                        if (player.ended) {
                        
                          player.currentTime = 0;
                        }
                        
                        player.play();
                      } else {
                      
                        player.pause();
                      }
                      
                      return false;
                    }
                  );
                  
    $item.bind({
      removed: function() {
      
        player.src = null;
      }
    });
                  
    $seekProgress.bind("progressChanged", function(e) {
    
      player.currentTime = player.duration * e.progress;
    });
                  
    $muteButton.text("Stumm")
               .attr("title", "Lautstärke stummschalten")
               .click(function() {
                              
                 player.muted = !player.muted;
                 
                 var text = "Stumm";
                 var title = "Lautstärke stummschalten";
                 
                 if (player.muted) {
                 
                   text = "Normal";
                   title = "Lautstärke wiederherstellen";
                 }
               
                 $muteButton.text(text)
                            .attr("title", title)
                            .toggleClass("active");
               });
    $volumeWidget.bind("volumeChanged", function(e) {
    
      player.volume = e.volume;
    });
    
    $audio.bind({
      playing: function() {
      
        $playPauseButton.removeClass("play")
                        .addClass("pause")
                        .text("Pause")
                        .attr("title", "Wiedergabe unterbrechen");
      },
      pause: function() {
      
        $playPauseButton.removeClass("pause")
                        .addClass("play")
                        .text("Fortsetzen")
                        .attr("title", "Wiedergabe fortsetzen");
      },
      ended: function() {
      
        $playPauseButton.removeClass("pause")
                        .addClass("play")
                        .text("Wiedergabe")
                        .attr("title", "Wiedergabe starten");
                        
        $durationLabel.text(durationFormatter.format(0));
      },
      timeupdate: function() {
        
        $durationLabel.html(durationFormatter.format(player.currentTime) +
                            "&nbsp;/&nbsp;" +
                            durationFormatter.format(player.duration));
      },
      loadedmetadata: function() {
      
        if (!"mozFrameBufferLength" in player) return;
      
        $equalizer.slideDown();
        // Use proposed Audio Data API to visualize audio output
        // See: https://wiki.mozilla.org/Audio_Data_API
        var frameBufferLength = player.mozFrameBufferLength;
        var channels = player.mozChannels;
        var fft = new FFT(frameBufferLength / channels, player.mozSampleRate);
        var context = $equalizer[0].getContext("2d");
        context.fillStyle = "#ffffff";
      
        // Process signals and visualize them
        $audio.bind("MozAudioAvailable", function(e) {
        
          var e = e.originalEvent;
          // Prefer Float32Array if available
          var signal = new window[(("Float32Array" in window) ? "Float32Array" : "Array")](frameBufferLength / channels);
          
          // Merge multiple channel signals via average
          for (var i = 0; i < frameBufferLength / channels; ++i) {
          
            signal[i] = (e.frameBuffer[channels * i] + e.frameBuffer[channels * i + 1]) / channels;
          }
          
          fft.forward(signal);
          
          var equalizerHeight = $equalizer[0].height;
          
          context.clearRect(0, 0, $equalizer.width(), $equalizer.width());
          
          for (var i = 0; i < fft.spectrum.length; ++i) {
          
            var normalizedValue = Math.max(0, Math.min((fft.spectrum[i] * 10), 1));
            var rectangle = new Rectangle(
              i * 5, equalizerHeight,
              4, equalizerHeight * normalizedValue
            );
          
            context.fillRect(rectangle.left, rectangle.top, rectangle.width, -rectangle.height);
          }
        });
      }
    });
    
    $audio.attr({
            src: resource.fullURL,
            autoplay: "autoplay"
          })
         // Safari (4, 5) needs Quicktime to be installed
         .html("<code>audio</code>-Element nicht verfügbar.");
         
    $player.append($audio)
           .append($playPauseButton)
           .append($seekProgress)
           .append($durationLabel)
           .append($volumeWidget)
           .append($muteButton);

    $item.append($equalizer.hide())
         .append($player);
    
    return $item;
  };
  
  this.toString = function() { return "[object AudioResourceFormatter]" };
}
AudioResourceFormatter.prototype = new MediaResourceFormatter;
providers.addProvider("resourceFormatters", "audio", new AudioResourceFormatter());

/*
 * Resource display formatter for image types
 */
function ImageResourceFormatter() {

  this.format = function(resource) {
  
    var $item = ImageResourceFormatter.prototype.format.call(
      this, resource, resource.fullURL);
    
    $item.attr("alt", resource.properties.title)
         .data("enlarge", true);
    
    return $item;
  };
  
  this.toString = function() { return "[object ImageResourceFormatter]" };
}
ImageResourceFormatter.prototype = new ImageFormatter;
providers.addProvider("resourceFormatters", "image", new ImageResourceFormatter());

/*
 * Resource display formatter for plain text
 */
function PlainTextResourceFormatter() {

  this.format = function(resource) {
  
    var $item = $("<iframe/>");
  
    $item.attr({
      src: resource.fullURL,
      seamless: "seamless",
      height: 600,
      width: 400
    });
    
    return $item;
  };
  
  this.toString = function() { return "[object PlainTextResourceFormatter]" };
}
PlainTextResourceFormatter.prototype = new ResourceFormatter;
providers.addProvider("resourceFormatters", "text/plain", new PlainTextResourceFormatter());

/*
 * Resource display formatter for video types
 */
function VideoResourceFormatter() {

  this.format = function(resource) {
  
    var $item = $("<video/>");
    
    $item.bind({
      removed: function() {
      
        $item[0].src = null;
      }
    });
  
    $item.attr({
           src: resource.fullURL,
           autoplay: "autoplay",
           controls: "controls", // TODO: custom controls
         })
         // Safari (4, 5) needs Quicktime to be installed
         .html("<code>video</code>-Element nicht verfügbar.")
         .data("enlarge", true);
    
    return $item;
  };
  
  this.toString = function() { return "[object VideoResourceFormatter]" };
}
VideoResourceFormatter.prototype = new MediaResourceFormatter;
providers.addProvider("resourceFormatters", "video", new VideoResourceFormatter());

/************************* Property formatters ************************/

/*
 * Base class for property value formatting
 */
function PropertyFormatter() {

  this.format = function(resource, value) {
  
    if (!value) {
    
      value = "&nbsp;";
    }
    
    return value;
  };
  
  this.sort = function($propertyA, $propertyB, order) {
  
    var textA = $propertyA.text().toLowerCase();
    var textB = $propertyB.text().toLowerCase();
    
    // Order locale specific by default
    return textA.localeCompare(textB) * (order == Sorting.ASCENDING ? 1 : -1);
  };
  
  this.toString = function() { return "[object PropertyFormatter]" };
}
PropertyFormatter.prototype = new Formatter;

/*
 * Property formatter for the resource thumbnail
 */
function ThumbnailPropertyFormatter() {

  this.format = function(resource, value) {
  
    var $thumbnail = ThumbnailPropertyFormatter.prototype.format.call(
      this, resource, value);
    
    // TODO: Width and height maybe configurable
    $thumbnail.attr({width: 100, height: 100});
    
    return $thumbnail;
  };
  
  this.toString = function() { return "[object ThumbnailPropertyFormatter]" };
}
ThumbnailPropertyFormatter.prototype = new ImageFormatter;

/*
 * Property formatter for the resource title
 */
function TitlePropertyFormatter() {

  this.format = function(resource, value) {
  
    return "<strong>" + value + "</strong>";
  };
  
  this.toString = function() { return "[object TitlePropertyFormatter]" };
}
TitlePropertyFormatter.prototype = new PropertyFormatter;
providers.addProvider("propertyFormatters", "title", new TitlePropertyFormatter());

/*
 * Property formatter for the resource size
 */
function SizePropertyFormatter() {

  var scale = 1024;
  var units = ["TB", "GB", "MB", "KB", "Bytes"];
  var maximumSize = Math.pow(scale, units.length - 1);

  this.format = function(resource, value) {
  
    var size = parseInt(value);
    
    if (!isNaN(size)) {
  
      var max = maximumSize;
      
      for (var i = 0; i < units.length; ++i) {
      
        // If the value exceeds the current unit, convert
        // to the unit, add the unit text and return this
        if (value > max) {
        
          return ((value / max).toFixed(2) + " ").replace(".", ",") + units[i];
        }
        
        // Proceed to the next lower unit (TB / 1024 = GB, ...)
        max /= scale;
      }
    }
    
    return SizePropertyFormatter.prototype.format.call(this, resource, value);
  };
  
  this.sort = function($propertyA, $propertyB, order) {
  
    var valueA = $propertyA.data("value");
    var valueB = $propertyB.data("value");
  
    return (valueA - valueB) * (order == Sorting.ascending ? 1 : -1);
  };
  
  this.toString = function() { return "[object SizePropertyFormatter]" };
}
SizePropertyFormatter.prototype = new PropertyFormatter;
providers.addProvider("propertyFormatters", "size", new SizePropertyFormatter());

/*
 * Property formatter for the resource type
 */
function TypePropertyFormatter() {

  var types = {
    "application/msword": "Microsoft Word-Dokument",
    "application/x-executable": "Anwendung",
    "application/x-msdos-program": "Anwendung",
    "application/x-msi": "Windows-Installer-Paket",
    "application/xhtml+xml": "XHTML-Dokument",
    "application/pdf": "PDF-Dokument",
    "application/zip": "ZIP-Archiv",
    "audio": "Audiostück",
    "audio/mpeg": "MP3-Audio",
    "audio/ogg": "OGG-Audio",
    "image": "Bild",
    "image/gif": "GIF-Bild",
    "image/jpeg": "JPEG-Bild",
    "image/png": "PNG-Bild",
    "text": "Text",
    "text/html": "HTML-Dokument",
    "text/rtf": "RichText-Dokument",
    "video": "Video",
    "video/mp4": "MP4-Video",
    "video/ogg": "OGG-Video",
    "video/quicktime": "Quicktime-Video"
  };

  this.format = function(resource, value) {
  
    var $item = $("<span/>");
  
    if (value in types) { // Explicit type
    
      $item.text(types[value]);
    } else { // General type
    
      var type = value.split("/")[0];
      
      if (type in types) {
      
        $item.text(types[type]);
      }
    }
    
    if ($item.text()) {
    
      $item.attr("title", value);
    } else {
    
      $item.text(TypePropertyFormatter.prototype.format.call(this, resource, value));
    }
    
    return $item;
  };
  
  this.toString = function() { return "[object TypePropertyFormatter]" };
}
TypePropertyFormatter.prototype = new PropertyFormatter;
providers.addProvider("propertyFormatters", "type", new TypePropertyFormatter());

function RatingPropertyFormatter() {

  this.format = function(resource, value) {
  
    return new RatingWidget(value);
  };

  this.sort = function($propertyA, $propertyB, order) {
  
    var widgetA = $propertyA.children()[0];
    var widgetB = $propertyB.children()[0];
  
    return (widgetA.rating - widgetB.rating) * (order == Sorting.ascending ? 1 : -1);
  };
  
  this.toString = function() { return "[object RatingPropertyFormatter]" };
}
RatingPropertyFormatter.prototype = new PropertyFormatter;
providers.addProvider("propertyFormatters", "rating", new RatingPropertyFormatter());
