package net.osmand.osm.util;

import net.osmand.Algoritms;
import net.osmand.data.DataTileManager;
import net.osmand.impl.ConsoleProgressImplementation;
import net.osmand.osm.Entity;
import net.osmand.osm.Entity.EntityId;
import net.osmand.osm.EntityInfo;
import net.osmand.osm.LatLon;
import net.osmand.osm.MapUtils;
import net.osmand.osm.Node;
import net.osmand.osm.OpeningHoursParser;
import net.osmand.osm.OpeningHoursParser.BasicDayOpeningHourRule;
import net.osmand.osm.OpeningHoursParser.OpeningHoursRule;
import net.osmand.osm.io.IOsmStorageFilter;
import net.osmand.osm.io.OsmBaseStorage;
import net.osmand.osm.io.OsmStorageWriter;
import net.osmand.swing.DataExtractionSettings;
import net.osmand.swing.MapPanel;
import net.osmand.swing.MapPointsLayer;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.xml.sax.SAXException;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.swing.JFrame;
import javax.swing.JMenuBar;
import javax.swing.UIManager;
import javax.xml.stream.XMLStreamException;


/**
 * Downloads list of Albert Heijn supermarkets and converts them to a map.
 * 
 * Source for json is:
 * http://www.ah.nl/albertheijn/winkelinformatie/winkels
 * 
 * @author Andrei Adamian
 *
 */
public class AHSupermarketResolver {
	
	private Log log = LogFactory.getLog(AHSupermarketResolver.class);
	
	public Map<String, Map<String, Object>> getSupermarkets() throws IOException {
		InputStream stream = openStream();
		try {
			char[] bytes = new char[1024];
			new InputStreamReader(stream).read(bytes);
			JSONArray object = new JSONArray(new JSONTokener(bytes.toString()));
			return parseJSON(object);
		} catch (JSONException t) {
			throw new IllegalStateException("Can't read json stream",t);
		} finally {
			try {
				stream.close();
			} catch (IOException e) {
			}
		}
	}
	
	@SuppressWarnings("unchecked")
	private Map<String, Map<String, Object>> parseJSON(JSONArray array) {
		HashMap<String,Map<String, Object>> map = new LinkedHashMap<String, Map<String, Object>>();
		for (int i=0;i<array.length();i++) {
			try {
				JSONObject object = (JSONObject) array.get(i);
				String id = object.getString("no");
				if (id != null)  {
					Iterator<String> keys = object.keys();
					Map<String, Object> properties = new LinkedHashMap<String, Object>();
					while (keys.hasNext()) {
						String name = keys.next();
						properties.put(name, object.get(name));
					}
					map.put(id, properties);
				} else {
					log.warn(String.format("Shop entry '%s' has been skipped, because 'no' property was not found", object));
				}
			} catch (JSONException e) {
				log.warn("Can't get object at index "+i,e);
			}
		}
		return map;
	}

	protected InputStream openStream() throws IOException {
		URL url = new URL("http://www.ah.nl/albertheijn/winkelinformatie/winkels");
		return url.openStream();
	}
	
	
	public static void selfTest() throws IOException {
		final String json = "[{city:'Eindhoven',lat:51.443278,format:'TOGO',lng:5.480161,sunday:true,street:'Neckerspoel',hours:[{F:'0630',U:'2300',D:'09-08-2010'},{F:'0630',U:'2300',D:'10-08-2010'},{F:'0630',U:'2300',D:'11-08-2010'},{F:'0630',U:'2300',D:'12-08-2010'},{F:'0630',U:'2330',D:'13-08-2010'},{F:'0700',U:'2330',D:'14-08-2010'},{F:'0800',U:'2300',D:'15-08-2010'},{F:'0630',U:'2300',D:'16-08-2010'},{F:'0630',U:'2300',D:'17-08-2010'},{F:'0630',U:'2300',D:'18-08-2010'},{F:'0630',U:'2300',D:'19-08-2010'},{F:'0630',U:'2330',D:'20-08-2010'},{F:'0700',U:'2330',D:'21-08-2010'},{F:'0800',U:'2300',D:'22-08-2010'}],no:5816,phone:'040-2376060',zip:'5611 AD',housenr:'10'},{city:'Amsterdam',lat:52.346837,format:'TOGO',lng:4.918422,sunday:true,street:'Julianaplein',hours:[{F:'0630',U:'2359',D:'09-08-2010'},{F:'0630',U:'2359',D:'10-08-2010'},{F:'0630',U:'2359',D:'11-08-2010'},{F:'0630',U:'2359',D:'12-08-2010'},{F:'0630',U:'2359',D:'13-08-2010'},{F:'0700',U:'2359',D:'14-08-2010'},{F:'0900',U:'2359',D:'15-08-2010'},{F:'0630',U:'2359',D:'16-08-2010'},{F:'0630',U:'2359',D:'17-08-2010'},{F:'0630',U:'2359',D:'18-08-2010'},{F:'0630',U:'2359',D:'19-08-2010'},{F:'0630',U:'2359',D:'20-08-2010'},{F:'0700',U:'2359',D:'21-08-2010'},{F:'0900',U:'2359',D:'22-08-2010'}],no:5817,phone:'020-4689944',zip:'1097 DN',housenr:'1'}]";
		AHSupermarketResolver resolver = new AHSupermarketResolver() {
			@Override
			protected InputStream openStream() throws IOException {
				return new ByteArrayInputStream(json.getBytes());
			}
		};
		Map<String, Map<String, Object>> map = resolver.getSupermarkets();
		assert map.size() == 2;
		assert map.containsKey("5816");
		assert map.containsKey("5817");
		// further  checks
		
		Map<String, Object> shop = map.get("5816");
		assert "Eindhoven".equals(shop.get("city"));
		
		shop = map.get("5817");
		assert "Amsterdam".equals(shop.get("city"));
	}
	
	
	// this file could be retrieved using xapi
	// http://xapi.openstreetmap.org/api/0.6/*[shop=supermarket][bbox=2.5,50,7.8,53.5]
	public void updateOSMFile(String pathToOsmFile, String pathToModifiedFile, boolean show) throws IOException, SAXException, XMLStreamException, JSONException{
		OsmBaseStorage storage = new OsmBaseStorage();
		final Map<String, EntityId> winkelNumbers = new LinkedHashMap<String, EntityId>();
		
		storage.getFilters().add(new IOsmStorageFilter(){

			@Override
			public boolean acceptEntityToLoad(OsmBaseStorage storage, EntityId entityId, Entity entity) {
				if(entity.getTag("winkelnummer") !=null && entity.getTag("name").contains("eijn")){
					winkelNumbers.put(entity.getTag("winkelnummer"), entityId);
					return true;
				}
				// register all nodes in order to operate with ways
				return true;
			}
			
		});
		storage.parseOSM(new FileInputStream(pathToOsmFile), new ConsoleProgressImplementation(2), null, true);
		Map<String, Map<String, Object>> supermarkets = getSupermarkets();
		
		DataTileManager<Entity> deleted = new DataTileManager<Entity>();
		
		for(String s : winkelNumbers.keySet()){
			if(!supermarkets.containsKey(s)){
				System.err.println("Shop " + s + " id=" +winkelNumbers.get(s) + " doesn't present on the site.");
				EntityId e = winkelNumbers.get(s);
				Entity en = storage.getRegisteredEntities().get(e);
				deleted.registerObject(en.getLatLon().getLatitude(), en.getLatLon().getLongitude(), 
						en);
			}
		}
		
		DataTileManager<Entity> notCorrelated = new DataTileManager<Entity>();
		DataTileManager<Entity> notShown = new DataTileManager<Entity>();
		
		for(String s : supermarkets.keySet()){
			Map<String, Object> props = supermarkets.get(s);
			if(winkelNumbers.get(s) != null){
				EntityId id = winkelNumbers.get(s);
				Entity e = storage.getRegisteredEntities().get(id);
				EntityInfo info = storage.getRegisteredEntityInfo().get(id);
				Map<String, String> newTags = new LinkedHashMap<String, String>();
				String p = props.get("format")+"";
				//IMPORTANT : comment what information should be updated or check
				String name = "Albert Heijn";
				if(!p.equals("AH")){
					name += " " + p;
				}
				newTags.put("name", name);
				newTags.put("phone", props.get("phone")+"");
				newTags.put("addr:city", props.get("city")+"");
				newTags.put("addr:street", props.get("street")+"");
				newTags.put("addr:housenumber", props.get("housenr")+"");
				newTags.put("addr:postcode", props.get("zip")+"");
				
				JSONArray o = (JSONArray) props.get("hours");
				List<OpeningHoursParser.OpeningHoursRule> rules = new ArrayList<OpeningHoursRule>();
				BasicDayOpeningHourRule prev = null;
				for(int i=0; i<7; i++){
					JSONObject obj = o.getJSONObject(i);
					
					if(!obj.isNull("C") && obj.getBoolean("C")){
					} else {
						String opened  = obj.get("F")+"";
						String closed = obj.get("U")+"";
						int start = Integer.parseInt(opened.substring(0, 2)) * 60 + Integer.parseInt(opened.substring(2));
						int end = Integer.parseInt(closed.substring(0, 2)) * 60 + Integer.parseInt(closed.substring(2));
						if(prev != null && prev.getStartTime() == start && prev.getEndTime() == end){
							prev.getDays()[i] = true;
						} else {
							BasicDayOpeningHourRule rule = new OpeningHoursParser.BasicDayOpeningHourRule();
							rule.getDays()[i] = true;
							rule.setStartTime(start);
							rule.setEndTime(end);
							prev = rule;
							rules.add(rule);
						}
					}
					
				}
				newTags.put("opening_hours", OpeningHoursParser.toStringOpenedHours(rules));
				
				// Check distance to info
				LatLon real = new LatLon((Double)props.get("lat"), (Double) props.get("lng"));
				double dist = MapUtils.getDistance(e.getLatLon(), real);
				if(dist > 150){
					// TODO move shop ?
					System.err.println("Winkel number = " + s + " is too far from site info - " + dist + " m !!! " + real);
					if(dist > 300){
						notCorrelated.registerObject(real.getLatitude(), real.getLongitude(), e);
					}
				}
				boolean changed = false;
				for(String k : newTags.keySet()){
					String val = newTags.get(k);
					if(!Algoritms.objectEquals(val, e.getTag(k))){
						e.putTag(k, val);
						changed = true;
					}
				}
				if(changed){
					info.setAction("modify");
				}
			} else {
				// TODO?
				LatLon real = new LatLon((Double)props.get("lat"), (Double) props.get("lng"));
				System.err.println("Winkel number = " + s + " is not found in database !!! " + real);
				Node n = new Node(real.getLatitude(), real.getLongitude(), -1);
				n.putTag("winkelnummer", "REG : " + s);
				notShown.registerObject(real.getLatitude(), real.getLongitude(), n);
			}
			
		}
		
		OsmStorageWriter writer = new OsmStorageWriter();
		writer.saveStorage(new FileOutputStream(pathToModifiedFile), storage, null, true);
		if(show){
			try {
				UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
			} catch (Exception e) {
				e.printStackTrace();
			}
			
			final MapPanel panel = new MapPanel(DataExtractionSettings.getSettings().getTilesDirectory());
			panel.setFocusable(true);
			MapPointsLayer toAdd = panel.getLayer(MapPointsLayer.class);
			toAdd.setPoints(notShown);
			toAdd.setPointSize(5);
			toAdd.setTagToShow("winkelnummer");
			
			
			MapPointsLayer red = new MapPointsLayer();
			red.setPoints(deleted);
			red.setColor(Color.red);
			red.setPointSize(5);
			panel.addLayer(red);
			
			MapPointsLayer blue = new MapPointsLayer();
			blue.setPoints(notCorrelated);
			blue.setColor(Color.blue);
			blue.setPointSize(4);
			panel.addLayer(blue);
			
			
			JFrame frame = new JFrame("Map view");
		    
			
		    frame.addWindowListener(new WindowAdapter(){
		    	@Override
		    	public void windowClosing(WindowEvent e) {
		    		DataExtractionSettings settings = DataExtractionSettings.getSettings();
					settings.saveDefaultLocation(panel.getLatitude(), panel.getLongitude());
					settings.saveDefaultZoom(panel.getZoom());
		    		System.exit(0);
		    	}
		    });
		    Container content = frame.getContentPane();
		    content.add(panel, BorderLayout.CENTER);

		    JMenuBar bar = new JMenuBar();
		    bar.add(MapPanel.getMenuToChooseSource(panel));
		    frame.setJMenuBar(bar);
		    frame.setSize(512, 512);
		    frame.setVisible(true);
			
		}
	}
	
	public static void main(String[] args) throws IOException, SAXException, XMLStreamException, JSONException {
		AHSupermarketResolver resolver = new AHSupermarketResolver();
		resolver.updateOSMFile("e:/Information/OSM maps/osm_map/holl_supermarket.osm", "e:/Information/OSM maps/osm_map/ams_poi_mod.osm",
				true);
	}
}