import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:html' hide Entry;
import 'dart:js';
import '../properties.dart';
import '../utils/html/blob_util.dart';
import '../utils/html/js_util.dart';
import '../utils/http/http_util.dart';
import '../models/models.dart';


class StripTask {
  LibraryEntry entry;
  Blob pdf;
  
  StripTask({LibraryEntry this.entry, Blob this.pdf});
}



Queue<Map> queue = new Queue();

/// Strips the given entry and the given blob. If pdf is given, the pdf will
/// be sent to the server, where the references and metadata are extracted.
/// If pdf is not given, the entry is snet to the server, where a pdf is
/// automatically retrieved and the references are extracted.
/// Be aware, that this task is enqueued. 
void strip({LibraryEntry entry: null, Blob pdf: null}) {
  if (entry == null) return;
  
  entry.loading("Idle.");
  
  enqueueStripTask({'entry': entry, 'pdf': pdf});
}

/// Enqueues the given annotation to ensure, that the proper rev is used.
void enqueueStripTask(Map data) {
  queue.add(data);
  if (queue.length == 1) {
    processStripQueue();
  }
}

/// Processes the queue.
void processStripQueue() {
  if (queue.isEmpty) return;  
  
  Map data = queue.first;
  if (data == null) return;
  
  LibraryEntry entry = data['entry'];
  Blob pdf = data['pdf'];
  
  if (entry == null) {
    queue.removeFirst();
    processStripQueue();
    return;
  }
  
  if (pdf != null) {
    _stripPdf(entry, pdf).whenComplete(() {
      queue.removeFirst();
      processStripQueue();
    });
  } else {
    _stripEntry(entry).whenComplete(() {
      queue.removeFirst();
      processStripQueue();
    });
  }
}

/// This method retrieves the annotations, the references, the metadata and the
/// pdf for given entry.
Future _stripEntry(LibraryEntry entry) {    
  /// Dispatch the entry to server to get the metadata, pdf and references.
  return dispatchWithEntry(entry).then((res) {
    if (res == null || res['pdf'] == null) {
      entry.error("Pdf couldn't be retrieved.");
      return;
    }
    
    entry.enqueue(res['data']);
    entry.setPdf(res['pdf']);
    entry.setReferences(res['references']);
    
    // There are no annotations. because the pdf was retrieved from web.
    entry.success("Pdf successfully downloaded.");
  }).catchError((e) => entry.error("Supplements couldn't be retrieved"));
}

/// This method retrieves the metadata, references and annotations from given
/// pdf.
Future _stripPdf(LibraryEntry entry, Blob pdf) {      
  return Future.wait([
    // Extract the annotations (is executed on client)
    extractAnnotations(entry, pdf),
    // Dispatch the pdf to server to get the metadata and references.
    dispatchWithBlob(entry, pdf),
  ]).then((List res) {
    entry.setPdfAnnotations(res[0]);
    entry.enqueue(res[1]['data']);
    entry.setReferences(res[1]['references']);
    
    // There are no annotations. because the pdf was retrieved from web.
    entry.success("Metadata successfully retrieved.");
  }).catchError((e) => entry.error("Supplements couldn't be retrieved"));
}

// _____________________________________________________________________________

/// Extracts the annotations from pdf (given by blob).
Future<List<PdfAnnotation>> extractAnnotations(LibraryEntry entry, Blob blob) {
  Completer completer = new Completer();
  
  entry.loading("Extracting annotations.");
  
  toBase64(blob).then((String base64) {
    // Define the callback.
    handleAnnotations(JsObject error, [JsObject result]) {
      if (error != null) {
        completer.completeError(dartify(error)); 
      } else {
        List annots = [];
        List annotsData = dartify(result);
        for (Map annotData in annotsData) {
          annots.add(new PdfAnnotation.fromData(annotData));
        }
        completer.complete(annots);
      }
    }
    
    context.callMethod("extractNativeAnnotations", [base64, handleAnnotations]);   
  }).catchError(completer.completeError);
  
  return completer.future;
}

// _____________________________________________________________________________
// Methods, which requests the server.

/// Dispatches the given entry to server to get the missing supplements.
Future<Map> dispatchWithEntry(LibraryEntry entry) {
  Completer completer = new Completer();    
  
  if (entry == null) {
    completer.completeError("No entry is given");
  } else {
    entry.loading("Downloading pdf.");
    
    httpRequest(URL_META2PDF, method: 'POST', data: entry.toJson(), 
        timeout: TIMEOUT_META2PDF).then((result) {
      completer.complete(_handleDispatchResult(entry, result));
    });
  }
  return completer.future;
}

/// Dispatches the given pdf (given as blob) to server to get the missing 
/// supplements.
Future<Map> dispatchWithBlob(LibraryEntry entry, Blob pdf) {
  entry.loading("Retrieving metadata.");
  
  return toBase64(pdf).then((String base64) {
    return dispatchWithBase64(entry, base64);
  });
}

/// Dispatches the given pdf (given as base64 string) to server to get the 
/// missing supplements.
Future<Map> dispatchWithBase64(LibraryEntry entry, String base64) {
  Completer completer = new Completer();    
  
  entry.loading("Retrieving metadata.");
  
  if (base64 == null) {
    completer.completeError("No pdf is given");
  } else {
    httpRequest(URL_PDF2META, method: 'POST', data: base64, 
        timeout: TIMEOUT_PDF2META).then((result) {
      completer.complete(_handleDispatchResult(entry, result));
    }).catchError(completer.completeError);
  }
  
  return completer.future;
}

/// Handles a dispatch result.
Map _handleDispatchResult(LibraryEntry entry, Map httpResult) {
  Map data = JSON.decode(httpResult['response']);
  if (data == null || data.isEmpty) {
    return null;
  }
  
  Blob pdf = toBlob(data.remove('pdf'));
  List references = toReferences(entry, data.remove('references'));
     
  return {'data': data, 'references': references, 'pdf': pdf};
}

// TODO: Outsource.
List<ReferenceEntry> toReferences(LibraryEntry entry, List referencesData) {
  if (entry == null) return null;
  
  List<ReferenceEntry> references = [];
  for (var i = 0; i < referencesData.length; i++) {
    Map referenceData = referencesData[i];
    referenceData['positionInBibliography'] = i + 1;
    referenceData['parentId'] = entry.id;
    references.add(new ReferenceEntry.fromData(referenceData));
  }
  return references;
}