package btools.routingapp;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;

import android.app.Activity;
import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.os.AsyncTask;
import android.os.PowerManager;
import android.os.StatFs;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Toast;
import btools.mapaccess.PhysicalFile;
import btools.mapaccess.Rd5DiffManager;
import btools.mapaccess.Rd5DiffTool;
import btools.router.RoutingHelper;
import btools.util.ProgressListener;

public class BInstallerView extends View
{
  private static final int MASK_SELECTED_RD5 = 1;
  private static final int MASK_DELETED_RD5 = 2;
  private static final int MASK_INSTALLED_RD5 = 4;
  private static final int MASK_CURRENT_RD5 = 8;
	
    private int imgwOrig;
    private int imghOrig;
    private float scaleOrig;

    private int imgw;
    private int imgh;

    private float lastDownX;
    private float lastDownY;
    
    private Bitmap bmp;
    
    private float viewscale;
        
    private float[] testVector = new float[2];

    private int[] tileStatus;

    private boolean tilesVisible = false;
    
    private long availableSize;
    private String baseDir;
    
    private boolean isDownloading = false;
    private volatile boolean downloadCanceled = false;

    private long currentDownloadSize;
    private String currentDownloadFile = "";
    private volatile String currentDownloadOperation = "";
    private String downloadAction = "";
    private volatile String newDownloadAction = "";

    private long totalSize = 0;
    private long rd5Tiles = 0;
    private long delTiles = 0;

    protected String baseNameForTile( int tileIndex )
    {
      int lon = (tileIndex % 72 ) * 5 - 180;
      int lat = (tileIndex / 72 ) * 5 - 90;
      String slon = lon < 0 ? "W" + (-lon) : "E" + lon;
      String slat = lat < 0 ? "S" + (-lat) : "N" + lat;
      return slon + "_" + slat;
    }
    
    private int gridPos2Tileindex( int ix, int iy )
    {
      return (35-iy)*72 + ( ix >= 70 ? ix-70: ix+2 );
    }
    
    private int tileForBaseName( String basename )
    {
      String uname = basename.toUpperCase();	
      int idx = uname.indexOf( "_" );
      if ( idx < 0 ) return -1;
      String slon = uname.substring( 0, idx ); 
      String slat = uname.substring( idx+1 );
      int ilon = slon.charAt(0) == 'W' ? -Integer.valueOf( slon.substring(1) ) :
    	       ( slon.charAt(0) == 'E' ?  Integer.valueOf( slon.substring(1) ) : -1 );
      int ilat = slat.charAt(0) == 'S' ? -Integer.valueOf( slat.substring(1) ) :
	           ( slat.charAt(0) == 'N' ?  Integer.valueOf( slat.substring(1) ) : -1 );
      if ( ilon < -180 || ilon >= 180 || ilon % 5 != 0 ) return -1;
      if ( ilat < - 90 || ilat >=  90 || ilat % 5 != 0 ) return -1;
      return (ilon+180) / 5 + 72*((ilat+90)/5);
    }

    
    public boolean isDownloadCanceled()
    {
      return downloadCanceled;
    }
    
    private void toggleDownload()
    {
    	if ( isDownloading )
    	{
    		downloadCanceled = true;
    		downloadAction = "Canceling...";
    		return;
    	}

      if ( delTiles > 0 )
      {
        ( (BInstallerActivity) getContext() ).showConfirmDelete();
        return;
      }

        int tidx_min = -1;
        int min_size = Integer.MAX_VALUE;
    	
		// prepare download list
        for( int ix=0; ix<72; ix++ )
        {
            for( int iy=0; iy<36; iy++ )
            {
    	    	int tidx = gridPos2Tileindex( ix, iy );
        	      if ( ( tileStatus[tidx] & MASK_SELECTED_RD5 ) != 0 )
        	      {
                    int tilesize = BInstallerSizes.getRd5Size(tidx);
                    if ( tilesize > 0 && tilesize < min_size )
                    {
                      tidx_min = tidx;
                      min_size = tilesize;
                    }
        	      }
            }
        }
        if ( tidx_min != -1 )
        {
          tileStatus[tidx_min] ^= tileStatus[tidx_min] & MASK_SELECTED_RD5;
          startDownload( tidx_min );
        }
    }
    
    private void startDownload( int tileIndex )
    {
      
    	String namebase = baseNameForTile( tileIndex );
      String baseurl = "http://brouter.de/brouter/segments4/";
        currentDownloadFile = namebase + ".rd5";
        currentDownloadOperation = "Checking";
        String url = baseurl + currentDownloadFile;
    	isDownloading = true;
    	downloadCanceled = false;
    	currentDownloadSize = 0;
    	downloadAction = "Connecting... ";
        final DownloadTask downloadTask = new DownloadTask(getContext());
        downloadTask.execute( url );
    }

    public void downloadDone( boolean success )
    {
    	isDownloading = false;
    	if ( success )
    	{
          scanExistingFiles(); 
          toggleDownload(); // keep on if no error
    	}
    	invalidate();
    }

    private int tileIndex( float x, float y )
    {
        int ix = (int)(72.f * x / bmp.getWidth());
        int iy = (int)(36.f * y / bmp.getHeight());
        if ( ix >= 0 && ix < 72 && iy >= 0 && iy < 36 ) return gridPos2Tileindex(ix, iy);
        return -1;
    }

    private void clearTileSelection( int mask )
    {
	  // clear selection if zooming out
      for( int ix=0; ix<72; ix++ )
        for( int iy=0; iy<36; iy++ )
        {
    	  int tidx = gridPos2Tileindex( ix, iy );
      	  tileStatus[tidx] ^= tileStatus[tidx] & mask;
        }
    }
    
    // get back the current image scale
    private float currentScale()
    {
    	testVector[1] = 1.f;
    	mat.mapVectors(testVector);
    	return testVector[1] / viewscale;
    }
    
    private void deleteRawTracks()
    {
      File modeDir = new File( baseDir + "/brouter/modes" );
      String[] fileNames = modeDir.list();
      if ( fileNames == null ) return;
      for( String fileName : fileNames )
      {
        if ( fileName.endsWith( "_rawtrack.dat" ) )
        {
          File f = new File( modeDir, fileName );
          f.delete();
        }
      }
    }

    private void scanExistingFiles()
    {
      clearTileSelection( MASK_INSTALLED_RD5 | MASK_CURRENT_RD5 );

      scanExistingFiles( new File( baseDir + "/brouter/segments4" ) );
      
      File secondary = RoutingHelper.getSecondarySegmentDir( baseDir + "/brouter/segments4" );
      if ( secondary != null )
      {
          scanExistingFiles( secondary );
      }

      availableSize = -1;
      try
      {
        StatFs stat = new StatFs(baseDir);
        availableSize = (long)stat.getAvailableBlocks()*stat.getBlockSize();
      }
      catch (Exception e) { /* ignore */ }
    }

    private void scanExistingFiles( File dir )
    {
        String[] fileNames = dir.list();
        if ( fileNames == null ) return;
        String suffix = ".rd5";
        for( String fileName : fileNames )
        {
          if ( fileName.endsWith( suffix ) )
          {
        	 String basename = fileName.substring( 0, fileName.length() - suffix.length() );
        	 int tidx = tileForBaseName( basename );
        	 tileStatus[tidx] |= MASK_INSTALLED_RD5;
        	 
        	 long age = System.currentTimeMillis() - new File( dir, fileName ).lastModified();
        	 if ( age < 10800000 ) tileStatus[tidx] |= MASK_CURRENT_RD5; // 3 hours
          }
        }
    }
    
    private Matrix mat;
    private Matrix matText;
    
       public void startInstaller() {

           baseDir = ConfigHelper.getBaseDir( getContext() );

           try
           {
             AssetManager assetManager = getContext().getAssets();
             InputStream istr = assetManager.open("world.png");
             bmp = BitmapFactory.decodeStream(istr);
             istr.close();
           }
           catch( IOException io )
           {
           	throw new RuntimeException( "cannot read world.png from assets" );
           }

           tileStatus = new int[72*36];
           scanExistingFiles();
           
           float scaleX = imgwOrig / ((float)bmp.getWidth());
           float scaley = imghOrig / ((float)bmp.getHeight());
           
           viewscale = scaleX < scaley ? scaleX : scaley;  
           
           mat = new Matrix();
           mat.postScale( viewscale, viewscale );
           tilesVisible = false;
        }

        public BInstallerView(Context context) {
            super(context);

            DisplayMetrics metrics = new DisplayMetrics();
            ((Activity)getContext()).getWindowManager().getDefaultDisplay().getMetrics(metrics);
            imgwOrig = metrics.widthPixels;
            imghOrig = metrics.heightPixels;
            int im = imgwOrig > imghOrig ? imgwOrig : imghOrig;
            
            scaleOrig = im / 480.f;            

            matText = new Matrix();
            matText.preScale( scaleOrig, scaleOrig );
            
            imgw = (int)(imgwOrig / scaleOrig);
            imgh = (int)(imghOrig / scaleOrig);
        }

        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        }

        private void toast( String msg )
        {
          Toast.makeText(getContext(),  msg, Toast.LENGTH_LONG ).show();
        }

         @Override
        protected void onDraw(Canvas canvas)
        {
          if ( !isDownloading )
          {
            canvas.setMatrix( mat );
            canvas.drawBitmap(bmp, 0,0, null);
          } 
          // draw 5*5 lattice starting at scale=3

          int iw = bmp.getWidth();
          int ih = bmp.getHeight();
          float fw = iw / 72.f;
          float fh = ih / 36.f;
          
          boolean drawGrid = tilesVisible && !isDownloading;

          if ( drawGrid )
          {
                
              Paint pnt_1 = new Paint();
              pnt_1.setColor(Color.GREEN);
            
              for( int ix=1; ix<72; ix++ )
              {
            	float fx = fw*ix;
                canvas.drawLine( fx, 0, fx, ih, pnt_1);
              }
              for( int iy=1; iy<36; iy++ )
              {
            	float fy = fh*iy;
                canvas.drawLine( 0, fy, iw, fy, pnt_1);
              }
          }
          rd5Tiles = 0;
          delTiles = 0;
          totalSize = 0;
              int mask2 = MASK_SELECTED_RD5 | MASK_DELETED_RD5 | MASK_INSTALLED_RD5;
              int mask3 = mask2 | MASK_CURRENT_RD5;
              Paint pnt_2 = new Paint();
              pnt_2.setColor(Color.GRAY);
              pnt_2.setStrokeWidth(1);
              drawSelectedTiles( canvas, pnt_2, fw, fh, MASK_INSTALLED_RD5, mask3, false, false, drawGrid );
              pnt_2.setColor(Color.BLUE);
              pnt_2.setStrokeWidth(1);
              drawSelectedTiles( canvas, pnt_2, fw, fh, MASK_INSTALLED_RD5 | MASK_CURRENT_RD5, mask3, false, false, drawGrid );
              pnt_2.setColor(Color.GREEN);
              pnt_2.setStrokeWidth(2);
              drawSelectedTiles( canvas, pnt_2, fw, fh, MASK_SELECTED_RD5, mask2, true, false, drawGrid );
              pnt_2.setColor(Color.YELLOW);
              pnt_2.setStrokeWidth(2);
              drawSelectedTiles( canvas, pnt_2, fw, fh, MASK_SELECTED_RD5 | MASK_INSTALLED_RD5, mask2, true, false, drawGrid );
              pnt_2.setColor(Color.RED);
              pnt_2.setStrokeWidth(2);
              drawSelectedTiles( canvas, pnt_2, fw, fh, MASK_DELETED_RD5 | MASK_INSTALLED_RD5, mask2, false, true, drawGrid );

        	canvas.setMatrix( matText );

            Paint paint = new Paint();
            paint.setColor(Color.RED);

            long mb = 1024*1024;

            if ( isDownloading )
           	{
              String sizeHint = currentDownloadSize > 0 ? " (" + ((currentDownloadSize + mb-1)/mb) + " MB)" : "";
              paint.setTextSize(30);
              canvas.drawText( currentDownloadOperation +  " " + currentDownloadFile + sizeHint, 30, (imgh/3)*2-30, paint);
              canvas.drawText( downloadAction, 30, (imgh/3)*2, paint);
           	}
            if ( !tilesVisible )
            {
                paint.setTextSize(35);
                canvas.drawText( "Touch region to zoom in!", 30, (imgh/3)*2, paint);
            }
            paint.setTextSize(20);
            
            
            
            String totmb = ((totalSize + mb-1)/mb) + " MB";
            String freemb = availableSize >= 0 ? ((availableSize + mb-1)/mb) + " MB" : "?";
            canvas.drawText( "Selected segments=" + rd5Tiles, 10, 25, paint );
            canvas.drawText( "Size=" + totmb + " Free=" + freemb , 10, 45, paint );


            String btnText = null;
            if ( isDownloading ) btnText = "Cancel Download";
            else if ( delTiles > 0 ) btnText = "Delete " + delTiles + " tiles";
            else if ( rd5Tiles > 0 ) btnText = "Start Download";
            
            if ( btnText != null )
            {
              canvas.drawLine( imgw-btnw, imgh-btnh, imgw-btnw, imgh-2, paint);
              canvas.drawLine( imgw-btnw, imgh-btnh, imgw-2, imgh-btnh, paint);
              canvas.drawLine( imgw-btnw, imgh-btnh, imgw-btnw, imgh-2, paint);
              canvas.drawLine( imgw-2, imgh-btnh, imgw-2, imgh-2, paint);
              canvas.drawLine( imgw-btnw, imgh-2, imgw-2, imgh-2, paint);
              canvas.drawText( btnText , imgw-btnw+5, imgh-10, paint );
            }
        }

        int btnh = 40;
        int btnw = 160;

        
float tx, ty;        

  private void drawSelectedTiles( Canvas canvas, Paint pnt, float fw, float fh, int status, int mask, boolean doCount, boolean cntDel, boolean doDraw )
  {
    for ( int ix = 0; ix < 72; ix++ )
      for ( int iy = 0; iy < 36; iy++ )
      {
        int tidx = gridPos2Tileindex( ix, iy );
        if ( ( tileStatus[tidx] & mask ) == status )
        {
          int tilesize = BInstallerSizes.getRd5Size( tidx );
          if ( tilesize > 0 )
          {
            if ( doCount )
            {
              rd5Tiles++;
              totalSize += BInstallerSizes.getRd5Size( tidx );
            }
            if ( cntDel )
            {
              delTiles++;
              totalSize += BInstallerSizes.getRd5Size( tidx );
            }
            if ( !doDraw )
              continue;
            // draw cross
            canvas.drawLine( fw * ix, fh * iy, fw * ( ix + 1 ), fh * ( iy + 1 ), pnt );
            canvas.drawLine( fw * ix, fh * ( iy + 1 ), fw * ( ix + 1 ), fh * iy, pnt );

            // draw frame
            canvas.drawLine( fw * ix, fh * iy, fw * ( ix + 1 ), fh * iy, pnt );
            canvas.drawLine( fw * ix, fh * ( iy + 1 ), fw * ( ix + 1 ), fh * ( iy + 1 ), pnt );
            canvas.drawLine( fw * ix, fh * iy, fw * ix, fh * ( iy + 1 ), pnt );
            canvas.drawLine( fw * ( ix + 1 ), fh * iy, fw * ( ix + 1 ), fh * ( iy + 1 ), pnt );
          }
        }
      }
  }

  public void deleteSelectedTiles()
  {
    for ( int ix = 0; ix < 72; ix++ )
    {
      for ( int iy = 0; iy < 36; iy++ )
      {
        int tidx = gridPos2Tileindex( ix, iy );
        if ( ( tileStatus[tidx] & MASK_DELETED_RD5 ) != 0 )
        {
          new File( baseDir + "/brouter/segments4/" + baseNameForTile( tidx ) + ".rd5").delete();
        }
      }
    }
    scanExistingFiles(); 
    invalidate();
  }

        @Override
        public boolean onTouchEvent(MotionEvent event) {
        	
            // get pointer index from the event object
            int pointerIndex = event.getActionIndex();

            // get pointer ID
            int pointerId = event.getPointerId(pointerIndex);

            // get masked (not specific to a pointer) action
            int maskedAction = event.getActionMasked();

            switch (maskedAction) {

            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_POINTER_DOWN: {
          	  lastDownX = event.getX();
          	  lastDownY = event.getY();
            	            	
              break;
            }
            case MotionEvent.ACTION_MOVE: { // a pointer was moved
            	
            	if ( isDownloading ) break;
            	int np = event.getPointerCount();
            	int nh = event.getHistorySize();
            	if ( nh == 0 ) break;

            	float x0 = event.getX( 0 );
            	float y0 = event.getY( 0 );
            	float hx0 = event.getHistoricalX(0,0);
            	float hy0 = event.getHistoricalY(0,0);

            	if ( np > 1 ) // multi-touch
            	{
                	float x1 = event.getX( 1 );
                	float y1 = event.getY( 1 );
                	float hx1 = event.getHistoricalX(1,0);
                	float hy1 = event.getHistoricalY(1,0);
                	
                	float r = (float)Math.sqrt( (x1-x0)*(x1-x0) + (y1-y0)*(y1-y0) );
                	float hr = (float)Math.sqrt( (hx1-hx0)*(hx1-hx0) + (hy1-hy0)*(hy1-hy0) );
                	
                	if ( hr > 0. )
                	{
                	  float ratio = r/hr;

                	  float mx = (x1+x0)/2.f;
                	  float my = (y1+y0)/2.f;

                	  float scale = currentScale();
                	  float newscale = scale*ratio;
                	  
                	  if ( newscale > 10.f ) ratio *= (10.f / newscale );
                	  if ( newscale < 0.5f ) ratio *= (0.5f / newscale );

                	  mat.postScale( ratio, ratio, mx, my );
                	                  	  
                	  mat.postScale( ratio, ratio, mx, my );

                	  boolean tilesv = currentScale() >= 3.f;
                	  if ( tilesVisible && !tilesv )
                	  {
                        clearTileSelection( MASK_SELECTED_RD5 | MASK_DELETED_RD5 );
                      }
                	  tilesVisible = tilesv;
                	}
                	
                	break;
            	}
                mat.postTranslate( x0-hx0, y0-hy0 );

                break;
            }
            case MotionEvent.ACTION_UP:
            	
            	long downTime = event.getEventTime() - event.getDownTime();
            	
            	if ( downTime < 5 || downTime > 500 )
            	{
            	  break;
            	}

            	if ( Math.abs(lastDownX - event.getX() ) > 10 || Math.abs(lastDownY - event.getY() ) > 10 )
            	{
              	  break;
              	}
            	
                // download button?
              if ( ( delTiles  > 0 || rd5Tiles  > 0 || isDownloading ) && event.getX() > imgwOrig - btnw*scaleOrig && event.getY() > imghOrig-btnh*scaleOrig )
            	{
            		toggleDownload();
                	invalidate();
            		break;
            	}

              if ( !tilesVisible )
              {
                float scale = currentScale();
                if ( scale > 0f && scale < 5f )
                {
                  float ratio = 5f / scale;
                  mat.postScale( ratio, ratio, event.getX(), event.getY() );
                  tilesVisible = true;
                }
                break;
              }

            	if ( isDownloading ) break;

            	Matrix imat = new Matrix();
            	if ( mat.invert(imat) )
            	{
            	  float[] touchpoint = new float[2];
            	  touchpoint[0] = event.getX();
            	  touchpoint[1] = event.getY();
            	  imat.mapPoints(touchpoint);
            	  
            	  int tidx = tileIndex( touchpoint[0], touchpoint[1] );
            	  if ( tidx != -1 )
            	  {
            	    if ( ( tileStatus[tidx] & MASK_SELECTED_RD5 ) != 0 )
            	    {
                    tileStatus[tidx] ^= MASK_SELECTED_RD5;
            	      if ( ( tileStatus[tidx] & MASK_INSTALLED_RD5 ) != 0 )
            	      {
            	        tileStatus[tidx] |= MASK_DELETED_RD5;
            	      }
            	    }
            	    else if ( ( tileStatus[tidx] & MASK_DELETED_RD5 ) != 0 )
                  {
                    tileStatus[tidx] ^= MASK_DELETED_RD5;
                  }
            	    else
            	    {
                    tileStatus[tidx] ^= MASK_SELECTED_RD5;
            	    }
            	  }
            	  
            	  tx = touchpoint[0];
            	  ty = touchpoint[1];
            	}
            	
            	
            	break;
            case MotionEvent.ACTION_POINTER_UP:
            case MotionEvent.ACTION_CANCEL: {
              // TODO use data
              break;
            }
            }
            invalidate();

            return true;
        }
        
        // usually, subclasses of AsyncTask are declared inside the activity class.
        // that way, you can easily modify the UI thread from here
        private class DownloadTask extends AsyncTask<String, Integer, String> implements ProgressListener {

            private Context context;
            private PowerManager.WakeLock mWakeLock;

            public DownloadTask(Context context) {
                this.context = context;
            }

            @Override
            public void updateProgress( String progress )
            {
              newDownloadAction = progress;
              publishProgress( 0 );
            }
  
            @Override
            public boolean isCanceled()
            {
              return isDownloadCanceled();
            }

              @Override
            protected String doInBackground(String... sUrls)
            {
              InputStream input = null;
              OutputStream output = null;
              HttpURLConnection connection = null;
              String surl = sUrls[0];
              String fname = null;
              File tmp_file = null;
              try
              {
                try
                {
                    int slidx = surl.lastIndexOf( "segments4/" );
                    String name = surl.substring( slidx+10 );
                    String surlBase = surl.substring( 0, slidx+10 );
                    fname = baseDir + "/brouter/segments4/" + name;

                    boolean delta = true;

                    File targetFile = new File( fname );
                    if ( targetFile.exists() )
                    {
                      updateProgress( "Calculating local checksum.." );
                    
                      // first check for a delta file
                      String md5 = Rd5DiffManager.getMD5( targetFile );
                      String surlDelta = surlBase + "diff/" + name.replace( ".rd5", "/" + md5 + ".rd5diff" );
                      
                      URL urlDelta = new URL(surlDelta);

                      updateProgress( "Connecting.." );

                      connection = (HttpURLConnection) urlDelta.openConnection();
                      connection.connect();

                      // 404 kind of expected here, means there's no delta file
                      if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND )
                      {
                        connection = null;
                      }
                    }                   
                
                    if ( connection == null )
                    {                
                      delta = false;
                      URL url = new URL(surl);
                      connection = (HttpURLConnection) url.openConnection();
                      connection.connect();
                    }
                    // expect HTTP 200 OK, so we don't mistakenly save error report
                    // instead of the file
                    if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
                        return "Server returned HTTP " + connection.getResponseCode()
                              + " " + connection.getResponseMessage();
                    }

                    // this will be useful to display download percentage
                    // might be -1: server did not report the length
                    int fileLength = connection.getContentLength();
                    currentDownloadSize = fileLength;
                    if ( availableSize >= 0 && fileLength > availableSize ) return "not enough space on sd-card";

                    currentDownloadOperation = delta ? "Updating" : "Loading";
                    
                    // download the file
                    input = connection.getInputStream();
                    
                    tmp_file = new File( fname + ( delta ? "_diff" : "_tmp" ) );
                    output = new FileOutputStream( tmp_file );

                    byte data[] = new byte[4096];
                    long total = 0;
                    long t0 = System.currentTimeMillis();
                    int count;
                    while ((count = input.read(data)) != -1) {
                        if (isDownloadCanceled()) {
                            return "Download canceled!";
                        }
                        total += count;
                        // publishing the progress....
                        if (fileLength > 0) // only if total length is known
                        {
                          int pct = (int) (total * 100 / fileLength);
                          updateProgress( "Progress " + pct + "%" );
                        }
                        else
                        {
                          updateProgress( "Progress (unnown size)" );
                        }

                        output.write(data, 0, count);
                        
                        // enforce < 2 Mbit/s
                        long dt = t0 + total/524 - System.currentTimeMillis();
                        if ( dt > 0  )
                        {
                        	try { Thread.sleep( dt ); } catch( InterruptedException ie ) {}
                        }
                    }
                    output.close();
                    output = null;

                    if ( delta )
                    {
                      updateProgress( "Applying delta.." );
                      File diffFile = tmp_file;
                      tmp_file = new File( fname + "_tmp" );
                      Rd5DiffTool.recoverFromDelta( targetFile, diffFile, tmp_file, this );
                      diffFile.delete();
                    }
                    if (isDownloadCanceled())
                    {
                      return "Canceled!";
                    }
                    if ( tmp_file != null )
                    {
                      updateProgress( "Verifying integrity.." );
                      String check_result = PhysicalFile.checkFileIntegrity( tmp_file );
                      if ( check_result != null ) return check_result;

                      if ( !tmp_file.renameTo( targetFile ) )
                      {
                        return "Could not rename to " + targetFile;
                      }
                      deleteRawTracks(); // invalidate raw-tracks after data update
                    }
                    return null;
                } catch (Exception e) {
                    return e.toString();
                } finally {
                    try {
                        if (output != null)
                            output.close();
                        if (input != null)
                            input.close();
                    } catch (IOException ignored) {
                    }

                    if (connection != null)
                        connection.disconnect();
                }
              }
              finally
              {
               	if ( tmp_file != null ) tmp_file.delete(); // just to be sure
              }
            }    

            @Override
            protected void onPreExecute() {
                super.onPreExecute();
                // take CPU lock to prevent CPU from going off if the user 
                // presses the power button during download
                PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
                mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, getClass().getName());
                mWakeLock.acquire();
            }

            @Override
            protected void onProgressUpdate(Integer... progress) {
            	if ( !newDownloadAction.equals( downloadAction ) )
            	{
            	  downloadAction = newDownloadAction;
            	  invalidate();
            	}
            }

            @Override
            protected void onPostExecute(String result) {
                mWakeLock.release();
                downloadDone( result == null );
                
                if (result != null)
                    Toast.makeText(context,"Download error: "+result, Toast.LENGTH_LONG).show();
                else
                    Toast.makeText(context,"File downloaded", Toast.LENGTH_SHORT).show();
            }
        
        } // download task
}